AR&VR&3D前端

WebGL学习笔记

2017-01-05  本文已影响453人  我不是传哥

WebGL从2012年开始接触,后面因为开始专注前端其他方面的事情,慢慢地就把它给遗忘。最近前端开始又流行起绘画制作,游戏、VR等等又开始引起前端人们的注意。所以,是时候开始重新拾起。
3D绘画是一个很复杂的数学物理综合体,会涉及到很多基础概念,了解了这些概念后才能进行实际的开发工作。

基础概念

<script id="vshader" type="x-shader/x-vertex">  
    ※顶点着色器  
</script>  
<script id="fshader" type="x-shader/x-fragment">  
    ※片段着色器  
</script> 

基础代码

页面初始化

<html>  
    <head>  
        <title>WebGL TEST</title>  
    </head>  
    <body>  
        <canvas id="canvas"></canvas>  
    </body>  
</html>

这段HTML代码,纯粹只是在页面上放置一个canvas,我们将从这个canvas中获取context,然后进行WebGL初始化。
获取context
首先获取canvas对象并设置其大小

var c = document.getElementById('canvas');  
c.width = 500;  
c.height = 300; 

获取WebGL的context

// 兼容处理
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');  

画面初始化
WebGL的context和普通的canvas是一样的,包含了绘画相关的各种各样的处理对象、函数、常量和属性。例如:

// 使用指定常量颜色来清空画面
gl.clear(gl.COLOR_BUFFER_BIT);
// 使用颜色值(RGBA)来清空画面
gl.clearColor(0.0,0.0,0.0,1.0);

上面几步简单的代码块就能够完成一个几步的WebGL使用,整个代码运行起来就是一个大小为500*300的黑色块画面。

认识GLSL

我们已经知道WebGL是无法利用固定渲染管线的,所以代替它的是可编辑渲染管线中的一种着色语言,叫做GLSL(OpenGL Shading Language)。
GLSL使用C语言为基础,并且有自己独立的语法。WebGL编程难点之一也就是这个GLSL的使用。

// attibute修饰符是用来接收不同顶点传来的不同信息
// vec*表示的是向量,*部分是一个2~4的数字,vec3表示的是一个3维的向量。其元素是浮点型
// position变量定义顶点信息
attribute vec3 position;  
void main(void) {  
    gl_Position = position;  
}

在WebGL中,顶点相关处理就是坐标变换,模型变换、视图变换和投影变换也就是顶点着色器的工作之一。一般来说,WebGL程序中,首先生成模型、视图、投影的各个矩阵,然后进行合并,最后将得到的坐标变换的矩阵传给顶点着色器。这时,我们定义传递这些矩阵值:

attribute vec3 position; 
// uniform修饰符是用来接收所有顶点一致的情报信息
// mat*表示的是方阵,可指定范围2~4,mat4表示的是4x4的方阵。其元素是浮点型
uniform mat4 mvpMatrix;  
void main(void) {  
    gl_Position = mvpMatrix * position;  
} 

顶点着色器与片段着色器的连接
GLSL里还有一个重要的修饰符,也就是varying修饰符,是用来连接顶点着色器和片段着色器之间的桥梁。
比如要把绘制的模型变成半透明,要怎么做?
方法虽然有很多,但是一般的做法是,向顶点里添加颜色的情报信息,然后通过操作颜色的透明度的变化来使模型半透明或者完全透明。这时候,如果想操作顶点里的颜色信息和画面上的颜色信息的话,就需要向片段着色器里传入一些必要的信息。首先是顶点着色器部分:

attribute vec4 position;  
attribute vec4 color;  
uniform mat4 mvpMatrix;  
varying vec4 vColor  
void main(void) {  
    vColor = color;  
    gl_Position = mvpMatrix * position;  
}

接着,片段着色器接收通过varying修饰符所定义的变量vColor:

varying vec4 vColor;  
void main(void)  {  
    gl_FragColor = vColor;  
}

和顶点着色器中必需要把数据传给gl_Position类似,片段着色器要把数据传给gl_FragColor,只是与顶点着色器不同的是,片段着色器的gl_FragColor不是必须要赋值的。但是一般都会输出一种什么颜色,所以gl_FragColor就变成必要的了。

顶点缓存

局部坐标
       顶点最终在画面上绘制的时候,要经过模型坐标变换,视图坐标变换和投影坐标变换,这个已经说过好多遍了。但是,在使用坐标情报之前,首先必须定义这些顶点群的构成,否则就没有办法开始了。定点群放到什么位置,就表现为坐标,一般叫做局部坐标。局部坐标就是模型的各个顶点相对于原点(x,y,z都为0)的坐标。比如,一个局部坐标为(1.0,0.0,0.0)的顶点,x轴方向距离原点的距离是1.0。同样,各个顶点都依次定义了局域坐标,这样顶点的位置就形成了。

顶点保存
       这些顶点的局部坐标,必须在WebGL程序中进行变换,然后传给顶点着色器。在WebGL中,为了处理这些顶点的信息,并将这些顶点信息保存,则需要使用顶点缓存。缓存(buffer),是表示数据保存空间的一般的计算机用语。WebGL中还有帧缓存,索引缓存等各种缓存,但是不管哪种缓存,你只需要把它想成保存数据的一块儿空间就行了。顶点缓存是其中的一种,就是用来保存顶点信息的,WebGL中的顶点缓存叫做VBO(vertex buffer object)。

顶点缓存和attribute
       WebGL的程序中,先把顶点的信息保存到VBO中,接着,通知着色器哪个VBO和哪个attribute变量相关,然后顶点着色器就可以正确的处理这些顶点了。根据前面的内容,顶点缓存相关的处理的具体流程如下:

VBO的生成过程中,首先在最初的时候必须把数据保存到数组中,因为顶点的信息(位置)中必须有x,y,z,所以数组的长度必须是顶点数x3,这个时候需要注意,数组不可以使用多维数组,VBO的生成需要使用一维数组。准备好保存顶点信息的数组之后,使用WebGL的context的方法生成VBO,当然生成的时候VBO是空的,然后将顶点信息的数组传给它。然后,比如把顶点着色器中的attribute函数和VBO关联起来。上面也说了,VBO中不是只能保存一种信息,位置情报以外的法线和颜色等信息存在的时候,要准备合适的VBO,然后通知WebGL哪个VBO和哪个attribute变量相关联。

矩阵计算和外部库

矩阵计算
       矩阵的计算方法,也不是什么特别奇怪复杂的东西,如果数学好好学习的话,没有基础也可以进行基本的矩阵计算。但是,如果不知道矩阵的加法和乘法运算的话,要进行稍微复杂一些的矩阵计算是非常难的。
矩阵的使用方法,并不是详细的计算方法。特别是在3D开发中,矩阵能够做什么,通过什么运算能得到什么样的结果,主要是掌握矩阵的使用方法,这一点很重要。

外部库
       DirectX和OpenGL中,内置了许多矩阵相关的处理,即使不使用外部库也可以进行矩阵计算。但是,WebGL中这些矩阵相关的计算是没有的,可能为了简化吧,当然,不是说没有办法了,而是,矩阵相关的一切计算,都需要自己来处理。话虽如此,但是WebGL中的矩阵计算还是一个很大的问题。数学好的人当然是没有问题了,但是对于其他人数学不太好的人就太困难了。但是,不用怕,有很多使用JavaScript写的矩阵计算的外部库,使用这些外部库的话,就算自己不会矩阵计算,也可以进行矩阵相关的处理,下面是其中的几个:

着色器的编译和连接

用个绘制多边形的例子来开始吧,首先,确认一下绘制的步骤:

步骤1、2和着色器的定义代码之前的“基础代码”和“认识GLSL”已经学习过,再次整理一下:

<html>  
  <head>  
      <title>WebGL TEST</title>  
  </head>  
  <body>  
      <canvas id="canvas"></canvas>
      <!-- ※顶点着色器 -->
      <script id="vs" type="x-shader/x-vertex">  
           attribute vec3 position;  
           uniform   mat4 mvpMatrix;  
           void main(void){  
              gl_Position = mvpMatrix * vec4(position, 1.0);  
           }  
      </script>  
      <!-- ※片段着色器 -->  
      <script id="fs" type="x-shader/x-fragment">  
            void main(void){  
              gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);  
            }  
      </script>
      <script>
           //首先获取canvas对象并设置其大小
           var c = document.getElementById('canvas');  
           c.width = 500;  
           c.height = 300;
           // 从canvas中获取WebGL的context
           var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
      </script>    
  </body>  
</html>

编译着色器
       编译也不需要什么特别的编译器,只需要调用WebGL内部的函数就可以进行编译了。准备一个函数,从着色器的编译,到实际着色器的生成这一连串的流程,都在这一个函数中来完成。下面是这个函数的代码:

function create_shader(id){  
    // 用来保存着色器的变量  
    var shader;  
    // 根据id从HTML中获取指定的script标签  
    var scriptElement = document.getElementById(id);  
    // 如果指定的script标签不存在,则返回  
    if(!scriptElement){return;}  
    // 判断script标签的type属性  
    switch(scriptElement.type){  
        // 顶点着色器的时候  
        case 'x-shader/x-vertex':  
            shader = gl.createShader(gl.VERTEX_SHADER);  
            break;  
        // 片段着色器的时候  
        case 'x-shader/x-fragment':  
            shader = gl.createShader(gl.FRAGMENT_SHADER);  
            break;  
        default :  
            return;  
    }  
    // 将标签中的代码分配给生成的着色器  
    gl.shaderSource(shader, scriptElement.text);  
    // 编译着色器  
    gl.compileShader(shader);  
    // 判断一下着色器是否编译成功  
    if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){  
        // 编译成功,则返回着色器  
        return shader;  
    }else{
        // 编译失败,弹出错误消息  
        alert(gl.getShaderInfoLog(shader));  
    }  
}  

程序对象的生成和连接
       使用varying修饰符定义的变量,可以从顶点着色器向片段着色器中传递数据。其实,实现从一个着色器向另一个着色器传递数据的,不是别的,就是程序对象。程序对象是管理顶点着色器和片段着色器,或者WebGL程序和各个着色器之间进行数据的互相通信的重要的对象。
那么,生成程序对象,并把着色器传给程序对象,然后连接着色器,将这些处理函数化:

function create_program(vs, fs){  
    // 程序对象的生成  
    var program = gl.createProgram();  
    // 向程序对象里分配着色器  
    gl.attachShader(program, vs);  
    gl.attachShader(program, fs);  
    // 将着色器连接  
    gl.linkProgram(program);  
    // 判断着色器的连接是否成功  
    if(gl.getProgramParameter(program, gl.LINK_STATUS)){
        // 成功的话,将程序对象设置为有效  
        gl.useProgram(program);  
        // 返回程序对象  
        return program;  
    }else{
        // 如果失败,弹出错误信息  
        alert(gl.getProgramInfoLog(program));  
    }  
}  

VBO的生成
      生成VBO的时候使用WebGL的createBuffer函数,这个函数就是用来生成缓存的。但是这个函数并不是用来直接生成VBO的,它只是生成了一个缓存对象,根据它里面保存的内容不同,用途也是不用的。
要操作缓存,首先必须跟WebGL进行绑定,就是说,要向“缓存”这个“光盘”中写入数据的时候,必须连接到WebGL这个“光驱”上。
绑定了缓存之后,使用bufferData函数来向缓存中写入数据,把这些处理写成一个函数,就是下面这样:

function create_vbo(data){  
    // 生成缓存对象  
    var vbo = gl.createBuffer();  
    // 绑定缓存  
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
    // 向缓存中写入数据  
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);    
    // 将绑定的缓存设为无效  
    gl.bindBuffer(gl.ARRAY_BUFFER, null); 
    // 返回生成的VBO  
    return vbo;  
}  

这个函数,接受一个矩阵作为参数,最后返回生成的VBO。首先使用createBuffer生成缓存对象,接着绑定缓存,然后写入数据。
绑定缓存的时候使用bindBuffer函数,这个函数有两个参数,第一个参数是缓存的类型,第二个参数是指定缓存对象。将第一个参数指定为gl.ARRAY_BUFFER就可以生成VBO。
另外,bufferData函数的第二个参数中出现的Float32Array对象,是javascript的类型数组,和一般的Array对象类似,是处理浮点型小数的时候使用的数组对象。3D世界里小数的精确度非常重要,所以使用类型数组来传递数据。而第三个参数中的gl.STATIC_DRAW这个常量,定义了这个缓存中内容的更新频率。VBO的话,模型数据基本上就是直接这么反复用,所以使用这个常量。
可以绑定WebGL的缓存,一次只能绑定一个,所以要操作其他的缓存的时候,必须要绑定相应的缓存。所以在函数的最后,再次使用bindBuffer函数,设定第二个参数为null,来将上次的绑定无效化,这是为了防止WebGL中的缓存一致保留,而出现和预想不一致的情况。
坐标变换矩阵的基本功能
      进行基本的3D渲染的时候,需要准备3个坐标变换矩阵。
第一个是模型变换矩阵,DirectX中叫做世界变换矩阵。模型变换矩阵影响的是所绘制的模型,模型的位置,模型的旋转,模型的放大和缩小等相关的情况。
第二个是视图变换矩阵,简单来说,就是定义拍摄3D空间的镜头(摄像机),决定了镜头的位置,镜头的参考点,镜头的方向等。
第三个是投影变换矩阵,这个坐标变换定义了屏幕的横竖比例,剪切的领域等,另外获取远近法则的效果也需要用这个变换矩阵。
根据这些内容,差不多知道了需要对矩阵进行哪些操作。使用minMatrix.js可以对矩阵进行基本的操作,来看一下minMatrix.js都能完成哪些操作吧。

var m = new matIV();

像上面这样,变量m就是matIV对象的一个实例,通过m.方法名可以调用matIV对象中存在的方法。
下面,列举一下minMatrix.js中定义的matIV对象的方法:

.create
函数: matIV.create()
参数: 无
返回值:    矩阵 
生成一个4x4的方阵,里面包含16个元素,其实是一个Float32Array对象,所有的元素都被初始化为0
.identity
函数: matIV.identity(dest)
参数: dest > 初始化的矩阵
返回值:    初始化后的矩阵
将接收的矩阵参数进行初始化并返回
.multiply
函数: matIV.multiply(mat1,mat2,dest)
参数: mat1 > 相乘的原始矩阵
参数: mat2 > 作为乘数的矩阵
参数: dest > 用来保存计算结果的矩阵
mat1在左,mat2在右,相乘后的结果保存到dest中
.scale
函数: matIV.scale(mat,vec,dest)
参数: mat > 原始矩阵
参数: vec > 缩放向量
参数: dest > 用来保存计算结果的矩阵
模型变换中的放大缩小,mat是原始矩阵,vec是X,Y,Z的各个缩放值组成的向量,最后的计算结果保存在dest中
.translate
函数: matIV.translate(mat,vec,dest)
参数: mat > 原始矩阵
参数: vec > 表示从原点开始移动一定距离的向量
参数: dest > 用来保存计算结果的矩阵
模型变换中的坐标移动,mat是原始矩阵,vec是X,Y,Z的各个方向上的移动量组成的向量,最后将计算结果保存到dest中
.rotate
函数: matIV.rotate(mat,angle,axis,dest)
参数: mat > 原始矩阵
参数: angle > 旋转角度
参数: axis > 旋转轴的向量
参数: dest > 用来保存计算结果的矩阵
模型变换中的旋转,mat是原始矩阵,angle是旋转角度,axis是旋转轴向量,最后将计算结果保存到dest中
.lookAt
函数: matIV.lookAt(eye,center,up,dest)
参数: eye > 镜头位置向量
参数: center > 镜头参考点的向量
参数: up > 镜头的方向向量
参数: dest > 用来保存计算结果的矩阵
视图变换矩阵的生成,eye是镜头在三维空间中的位置,center是这个镜头的参考点,up是镜头的方向向量,最后将计算结果保存到dest中
.perspective
函数: matIV.perspective(fovy,aspect,near,far,dest)
参数: fovy > 视角
参数: aspect > 屏幕的宽高比例
参数: near > 近截面的位置
参数: far > 远截面的位置
参数: dest > 用来保存计算结果的矩阵
投影变换矩阵的生成,这里生成的是一般被称为[透视射影]的投影变换矩阵,包含远近法则。fovy是视角,aspect是屏幕的横竖比例,near是近截面的位置(必须是大于0的数值),far远截面的位置(任意数值),最后将计算结果保存到dest中
.transpose
函数: matIV.transpose()
参数: mat > 原始矩阵
参数: dest > 用来保存计算结果的矩阵
矩阵的行列互换,将计算结果保存到dest中
.inverse
函数: matIV.inverse(mat,dest)
参数: mat > 原始矩阵
参数: dest > 用来保存计算结果的矩阵
求矩阵的逆矩阵,mat是原始矩阵,求的的逆矩阵保存到dest中
// 生成matIV对象  
var m = new matIV();  
// 矩阵生成及初始化  
var Matrix = m.identity(m.create());

(未完待续)
注:学习内容来自于https://wgld.org/

上一篇下一篇

猜你喜欢

热点阅读