Appearance
WebGL学习
WebGL历史
WebGL是基于OpenGL ES进行开发的,WebGL1.0版本基于OpenGL ES2.0,而WebGL2.0会基于OpenGL ES3.0。 那么OpenGL ES是撒?OpenGL ES是OpenGL的一个子库,主要是针对嵌入式计算机,智能手机和游戏设备等的子库。 最后OpenGL又是什么呢?OpenGL是计算机三维图形渲染的两大技术之一,另一个技术是大家都很熟悉了Directive3D(微软DirectiveX的一部分,反正经常会报错),Directive主要是针对Window系统的渲染,而OpenGL则被设计在跨平台的操作上了。 总的来说借鉴一下书上的OpenGL世代图,大致也就是这样:
OpenGL 1.0 ——> OpenGL ES 1.1 OpenGL 2.0 ——> OpenGL ES 2.0 ——> WebGL 1.0 OpenGL 3.3 ——> OpenGL ES 3.0 ——> WebGL 2.0 OpenGL 4.3
最后补充一点,在OpenGL2.0版本以后我们才可以对着色器(Shader)进行操作,操作着色器的语言是GLSL语言/GLSL ES语言(和C语言一样)
WebGL程序组成
javascript
function main(){
//---- canvas ----
//获取canvas节点
let canvas = document.querySelector('canvas')
//获取canvas上下文
let gl = canvas.getContext('webgl')
//---- GLSL程序部分
// Vertex Shader(顶点着色器)
let vertex_source = `
void main(){
gl_Position = vec4(0.0,0.0,0.0,1.0);
gl_PointSize = 10.0;
}
`
// Fragment Shader(片元着色器)
let fragment_source = `
void main(){
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`
//WebGL API调用部分
let program = initProgram(gl,vertex_source,fragment_source)
gl.clearColor(0.0,0.0,1.0,1.0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArray(gl.point,0,1)
//辅助方法:初始化着色器程序
function initProgram(gl,vsource,fsource){
let vShader = initShader(gl,gl.VERTEX_SHADER,vsource)
let fShader = initShader(gl,gl.FRAGMENT_SHADER,fsource)
//创建WebGL程序
let program = gl.createProgram()
gl.attachShader(program,vShader)
gl.attachShader(program,fShader)
gl.linkProgram(program)
//判断是否创建成功
if(!gl.getProgramParameter(program,gl.LINK_STATUS)){
console.log('program init false')
return
}
gl.useProgram(program)
return program
}
//辅助方法:初始化着色器
function initShader(gl,type,source){
let shader = gl.createShader(type)
gl.shaderSource(shader,source)
gl.compileShader(shader)
if(!gl.getShaderParameter(shader,gl.COMPILE_STATUS)){
console.log('error occured compiling shaders:' + gl.getShaderInfoLog(shader))
return null
}
return shader
}
}Javascript主程序
包含两个主要部分Canvas和WebGL API,WebGL利用了HTML5的Canvas来绘制图3D形
Canvas
Canvas是HTML5引入,用于构建2D,3D的图形,可以在<canvas>标签中增加文本,来判断浏览器是否支持<canvas>,因为不支持的浏览器将显示这些文本信息,而支持的将默认显示一个空的画布。 要使用WebGL,首先要利用Canvas获取到上下文,主要按照三步进行操作
获取页面的Canvas元素
利用Canvas元素获取上下文(WebGL现在可以直接使用getContext('gl')来获取,对于2d的我们常用getContext('2d')方法获取Canvas上下文)
利用WebGL API来操作绘制图形
WebGL API
WebGL API是WebGL编程的最主要部分,因为包含的API太多了,我也是新入门,所以目前暂时提及几个在例子中用到的主要函数:
cpp
// 清空缓冲区相关
gl.clearColor: 设置webgl颜色缓冲区清除后画布颜色
gl.clear: 清除webgl的缓冲区,可以有三个参数,分别为颜色缓冲(COLOR_BUFFER_BIT),深度缓冲区(DEPTH_BUFFER_BIT),模板缓冲区(STENCIL_BUFFER_BIT)
gl.drawArrays:利用缓冲区数据绘制图形
// 构建程序相关
gl.createProgram: 创建一个program对象
gl.attachShader: 将程序对象和shader进行绑定
gl.linkProgram: 编译程序?
gl.useProgram:设置使用的webgl program对象
// Shader构建相关
gl.createShader: 根据类型构建Shader
gl.shaderSource: 加载Shader的GLSL资源
gl.compileShader: 编译Shader返回shader对象
// 操作GLSL中变量相关,通常命名为:<方法名><变量个数><变量类型>
gl.getAttribLocation: 获取程序中的attribute变量的位置
gl.vertex[1,2,3,4]f: 设置attribute的值的函数族
gl.getUniformLocation: 获取程序中uniform变量的位置
gl.uniform[1,2,3,4]f: 设置uniform的值的函数族GLSL
构成WebGL的第二部分是GLSL程序,WebGL构建需要两个着色器Vertex Shader(顶点着色器)和Fragment Shader(片元着色器) 注:我其实比较喜欢用英文而不喜欢用中文来表述这两个着色器,因为现在英文文档较多,知道英文名比中文名要好。
Vertex Shader
中文译名顶点着色器,主要用来描述顶点信息(位置、尺寸等)。gl_Position和gl_PointSize是GLSL中的内置变量。
Fragment Shader
中文译名片元找色器,用途是用来描述绘制图形过程和光照等信息。gl_FragColor也是GLSL中的内置变量。
GLSL中的变量
我们绘制的点的位置和大小,颜色,硬编码写到GLSL程序中,而没办法通过页面操作进行改变了,那么如果我们想要GLSL和web页面惊醒交互,按么我们就要使用GLSL变量。 GLSL中变量的声明包含三部分:
<存储限定符><变量类型><变量名>
其中存储限定符有三个attribute(只用于vertex source中),uniform(可用于vertex和fragment source中),以及varying(用于vertex和fragment之间的数据传递); 变量类型有float,vec[1,2,3,4](分别代表有多少个矢量组成的数据结构); 变量名通常根据存储限定符来进行定义attribute就是a_,uniform就是u_ 于是代码就可以根据以下进行调整,来设置变量:
javascript
let vertex_source = `
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
}
`
let fragment_source = `
precision mediump float;//必须要指定精度
uniform vec4 u_FragColor;
void mian(){
gl_FragColor = u_FragColor;
}
`WebGL其他
坐标系转换
首先,WebGL使用的是笛卡尔坐标系,也就是说右手坐标系。就是x轴的正向是大拇指方向,y轴正向是四指方向,z轴方向为穿过掌背到掌心;
其次,WebGL中的坐标系的值是从[-1.0,1.0]的范围,原点为Canvas图形中心点;
最后,对于我们使用的web页面坐标系,最左上角为原点位置,面对屏幕时,x正方向往右,y轴正方向往下。
所以设置正确的WebGL点的坐标位置时,我们要将web页面的坐标信息转换为WebGL的坐标信息,转换过程如下:
javascript
let x = e.target.clientX;
let y = e.target.cliettY;
let rect = canvas.getBoundingClientRect();
x = (x - rect.left - rect.width/2) / (rect.width / 2);
y = (rect.height / 2 - (y-rect.top)) / (rect.height / 2);根据鼠标点击绘制点
javascript
function main(){
let canvas = document.querySelector('canvas')
let gl = canvas.getContext('webgl')
if(!gl){
console.log('获取canvas Context失败')
return
}
let vShader_source = `
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`
let fShader_source = `
void main(){
gl_FragColor = vec4(1.0,0.3,0.0,1.0);
}
`
if(!initShaders(gl,vShader_source,fShader_source)){
console.log('初始化着色器失败')
return
}
let a_Position = gl.getAttribLocation(gl.program,'a_Position')
gl.clearColor(1.0,0.0,0.0,1.0)
gl.clear(gl.COLOR_BUFFER_BIT)
let pointArray = []
canvas.mousedown = function(e){
click(gl,e,canvas,a_Position)
}
function click(gl,e,canvas,a_Position){
let x = e.clientX;
let y = e.clientY;
let rect = e.target.getBoundingClientRect();
x = (x - rect.left - rect.width/2) / (rect.width / 2);
y = (rect.height / 2 - (y-rect.top)) / (rect.height / 2);
pointArray.push([x,y])
gl.clear(gl.COLOR_BUFFER_BIT)
for(let i = 0,length = pointArray.length;i<length;i++){
gl.vertexAttrib2f(a_Position,pointArray[i][0],pointArray[i][1])
gl.drawArrays(gl.POINTS,0,1)
}
}
}图形绘制
利用缓冲区绘制多个点
WebGL中要使用缓冲区,主要有以下五个步骤
利用
gl.createBuffer()创建缓冲区对象利用
gl.bindBuffer()将创建的缓冲区对象和WebGL中的内置对象gl.ARRAYBUFFER进行绑定利用
gl.bufferData()给gl.ARRAY_BUFFER内置对象传递数据(数据不能直接传递给缓冲区对象,要通过gl.ARRAY_BUFFER来进行)利用
gl.vertexAttribPointer()将缓冲区数据分配给GLSL中的变量最后利用
gl.enableVertexAttribArray()开启缓冲区变量的使用,开启后gl.vertexAttrib[1234]f方法簇将失效
完成上述操作后,调用gl.drawArrays()就可以一次绘制缓冲区数据了,此时蒋第三个参数变为所需要的绘制点的数目就可以了。根据前一个例子,这里将drawPoint()进行调整为使用缓冲区
javascript
function initPoint(gl,a_Position){
let pointData = new FloatArray([-0.5,-0.5,0.0,0.5,0.5,-0.5])
// 创建缓冲区对象
let buffer = gl.createBuffer();
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER,buffer)
// 将数据传递到缓冲区
gl.bufferData(gl.ARRAY_BUFFER,pointData,gl.STATIC_DRAW)
// 将缓冲区数据传递给顶点着色器attribute属性
gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0)
// 激活顶点和缓冲区到连接
gl.enableVertexAttribArray(a_Position)
// 一次性绘制多个点
gl.drawArrays(gl.POINT,0,3)
}PS:代码中使用了Float32Array来创建点的数据对象,它是Javascript提供的一种类型化数组,目的是为了说明数组中的所有数据都是同一种数据类型的特殊数组,使处理数组效率更快(可能不用做类型判断和转化了),其中类型化数组提供了很多方法和属性,尤其BYTES_PER_ELEMENT属性在之后会有很大用处的。
利用mode控制图形绘制
在使用缓冲区的基础上绘制图形就很简单了,只需要改变gl.drawArrays()中的第一个参数就可以了。
WebGL的第一个参数mode,提供了7种值:
gl.POINT:绘制点
gl.LINES:绘制线段
gl.LINE_STRIP:绘制连续线段,例如传入[A0,A1,A2,A3]四个坐标信息,那么绘制结果为[A0,A1],[A1,A2],[A2,A3]
gl.LINE_LOOP:首尾两个点会连接起来
gl.TRIANGLES:绘制三角形
gl.TRIANLE_STRIP:绘制一系列三角形,例如传入[A0,A1,A2,A3,A4,A5]五个坐标信息,那么绘制结果为[A0, A1, A2], [A2, A1, A3], [A2, A3, A4], [A4, A3, A5](webGL中绘制是按照逆时针方式进行绘制的)
gl.TRIANGLE_FAN:以第一个点为所有三角形顶点,绘制三角扇
图形变换
我们经常会将绘制的图形进行平移、旋转、缩放操作,这些操作统一被称为仿射变换。
基本变换
平移
平移要实现的其实是方向坐标上的位移
javascript
x' = x + ux;
y' = y + uy;因为GLSL中矢量可以直接进行加减运算,所以修改顶点着色器程序:
javascript
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main(){
//矢量可以直接进行运算
gl_Position = a_Position + vec4(0.1,0.1,0.0,0.0);
gl_PointSize = a_PointSize;
}
`如果想要自定义平移距离,那么可以在顶点着色器程序中新增一个uniform变量(使用uniform变量是因为变量本身与顶点无关)
javascript
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
uniform vec4 u_Translate;
void main(){
//矢量直接进行运算
gl_Position = a_Position + u_Translate;
gl_PointSize = a_PointSize;
}
`
let u_Translate = gl.getUniformLocation(gl.program,'u_Translate');
gl.uniform4f(u_Translate,0.1,0.1,0.0,0.0);ps:要注意因为是使用齐次坐标,最后一个变量要传递为0.0(因为默认齐次坐标的第四个变量为1.0,矢量计算后最后一个变量任然应该为1.0)
缩放
缩放要实现的是方向坐标上的比例变化
javascript
x' = ux;
y' = uy;实现缩放的关键是要获取坐标在某些方向上的分量,可以通过a_Position.x和a_Position.y来分别获取x和y方向上的分量,修改着色器程序:
javascript
//着色器程序
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main(){
//分别设置四个方向上的分量信息
gl_Position.x = a_Position.x * 0.5;
gl_Position.y = a_Position.y * 0.5;
gl_Position.z = a_Position.z;
gl_Position.w = 1.0;
gl_PointSize = a_PointSize;
}
`旋转
旋转必须指明三个要素:旋转轴、旋转方向和旋转角度。
WebGL中旋转正方向是逆时针方向,遵循右手旋转法则,也就是大拇指朝向轴的正方向,四指方向就是旋转正方向。
以仅绕z轴旋转为例,那么如果坐标系中的点的原角度为a(与x轴正方向角度),转动角度为b,我们可以利用三角函数得到
javascript
/* --- 推导过程 --- */
x = r*cos(a);
y = r*sin(a);
/* --- 利用三角函数和角公式进行变化 --- */
x' = r*cos(a+b) = r*cos(a)*cos(b) - r*sin(a)*sin(b) = x*cos(b) - y*sin(b);
y' = r*sin(a+b) = r*cos(a)*sin(b) + r*sin(a)*cos(b) = x*sin(b) + y*cos(b);
/* --- 结果 --- */
x' = x * cosb - y * sinb;
y' = x * sinb + y * cosb;于是同样通过获取分量的方式,将顶点着色器程序进行修改
javascript
//着色器程序
let VSHADER_SOURCE = `
attribute vec4 a_Position;
uniform float u_Cosb,u_Sinb;
void main(){
// 分别设置四个方向上的分量信息
gl_Position.x = a_Position.x * u_Cosb - a_Position.y * u_Sinb
gl_Position.y = a_Position.x * u_Sinb + a_Position.y * u_Cosb;
gl_Position.z = a_Position.z;
gl_Position.w = 1.0;
gl_PointSize = 10.0;
}
`
//转动30度
let radian = 30 / 180 * Math.PI;
let u_Sinb = gl.getUniformLocation(gl.program,'u_Sinb')
let u_Cosb = gl.getuniformLocation(gl.program,'u_Cosb')
gl.unform1f(u_Sinb,Math.sin(radian))
gl.unform1f(u_Cosb,Math.cose(radian))