原文地址:WebGL学习(1) - 三角形

还记得第一次看到canvas的粒子特效的时候,真的把我给惊艳到了,原来在浏览器也能做出这么棒的效果。结合《HTML5 Canvas核心技术》和网上的教程,经过半年断断续续的学习,对canvas的学习终于完结,对常用的canvas特效基本能做到信手拈来的。canvas特效请看:样例列表

众所周知,canvas是2D绘图技术,虽然可以通过坐标变换,位置计算也能做到3D的效果。但3D场景数据量毕竟比2D要高一个数量级的,纯粹用canvas的话,不管是性能和开发的复杂度会成为一个瓶颈。

这也是webGL出现的原因,解决web端3D渲染的场景。webGL会调用到GPU,处理大量重复的3D场景数据时,性能非常有优势。同时webGL是基于openGL ES 2.0, 因此它处理3D场景是非常成熟的。但为什么不直接学习three.js呢?因为本人对图形学感兴趣,只是希望做一些自己喜欢的效果的同时深入了解计算机图形学,没指望通过它做商业项目。

为了让学习更有动力和目的性,我们以实例为导向学习webGL,再从中展开到需要学习哪些知识点。这次我们来实现如下的动画,该教程参考了《WebGL编程指南》

实际效果请看:旋转的三角形

webGL渲染流程

webGL的渲染流程如下,其中第2,3,4步是重点,里面细节比较多。接着我们就按这个流程一步一步解决问题

  1. 获取webGL绘图上下文
  2. 初始化着色器
  3. 创建、绑定缓冲区对象
  4. 向顶点着色器和片元着色器写入数据
  5. 设置canvas背景色,清空canvas
  6. 绘制

webGL绘图上下文

webGL是canvas基础之上的3D绘图技术,只是上下文不同,get3DContext函数作用就是依次降级获取上下文。

var canvas = document.getElementById("canvas"),
gl = get3DContext(canvas, true);
function get3DContext(canvas, opt) {
var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
var context = null;
for (var i = 0, len = names.length; i < len; i++) {
try {
context = canvas.getContext(names[i], opt);
} catch (e) {}
if (context) {
break;
}
}
return context;
}

着色器

着色器就是嵌入到js中的webGL代码,是由GLSL语言编写的,可以把着色器看成是js代码连接webGL的中间件。顶点着色器和片元着色器分别用于操作顶点和颜色光照,《WebGL编程指南》中是把着色器写成字符串,但从可维护性考虑,还是写在script标签中比较好。GLSL语言与C语言非常像,只要熟悉了GLSL特有的部分,其实还是比较简单的。

限定符

限定符只能用于全局变量,有3种类型:

  • attribute用于表示顶点信息
  • uniform用于表示除顶点外的其他信息,可以是除结构体和数组之外的任意类型
  • varying用于顶点着色器向片元着色器传输数据

GLSL特有的数据类型

  1. 向量:

    vec2, vec3, vec4 : 表示有2,3,4个浮点数的向量

    ivec2, ivec3, ivec4 : 表示有2,3,4个整形的向量

    bvec2, bvec3, bvec4 : 表示有2,3,4个布尔值的向量

  2. 矩阵:

    mat2, mat3, mat4 : 表示有2x2,3x3,4x4的浮点数的矩阵

顶点着色器

<script type="x-shader/x-vertex" id="vs">
attribute vec4 a_Position; //顶点,4个浮点的矢量,attribute变量传输与顶点有关的数据,表示逐顶点的信息
uniform mat4 u_xformMatrix; //变换矩阵,4*4浮点矩阵, uniform变量传输的是所有顶点都相同的数据
void main() {
gl_Position=u_xformMatrix*a_Position;
}
</script>

片元着色器

<script type="x-shader/x-fragment" id="fs">
precision mediump float; // 精度限定
uniform vec4 u_FragColor; // 颜色
void main() {
gl_FragColor = u_FragColor;
}
</script>

接着就是创建着色器了,首先从页面script标签取出着色器代码,初始化着色器;接着创建程序对象,最后连接程序对象。中间的步骤其实非常的啰嗦,已经把这几个步骤封装,我们只需要调用createShaders就可以了。

/**
* 根据script id创建着色器
* @param {Object} gl context
* @param {String} vid script id
* @param {String} fid script id
* @return {Boolen}
*/
function createShaders(gl, vid, fid) {
var vshader, fshader, element, program;
[vid, fid].forEach(function(id) {
element = document.getElementById(id);
if (element) {
switch (element.type) {
// 顶点着色器的时候
case "x-shader/x-vertex":
vshader = element.text;
break;
// 片段着色器的时候
case "x-shader/x-fragment":
fshader = element.text;
break;
default:
break;
}
}
});
if (!vshader) {
console.log("VERTEX_SHADER String not exist");
return false;
}
if (!fshader) {
console.log("FRAGMENT_SHADER String not exist");
return false;
}
program = createProgram(gl, vshader, fshader);
if (!program) {
console.log("Failed to create program");
return false;
} gl.useProgram(program);
gl.program = program;
return true;
} /**
* 创建连接程序对象
* @param {Object} gl 上下文
* @param {String} vshader 顶点着色器代码
* @param {String} fshader 片元着色器代码
* @return {Object}
*/
function createProgram(gl, vshader, fshader) {
// 创建着色器对象
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
} // 创建程序对象
var program = gl.createProgram();
if (!program) {
return null;
} // 连接着色器对象
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader); // 连接程序对象
gl.linkProgram(program); // 检查连接结果
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log("Failed to link program: " + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
} /**
* 加载着色器
* @param {Object} gl 上下文
* @param {Object} type 类型
* @param {String} source 代码字符串
* @return {Object}
*/
function loadShader(gl, type, source) {
// 创建着色器对象
var shader = gl.createShader(type);
if (shader == null) {
console.log("unable to create shader");
return null;
} // 设置着色器程序
gl.shaderSource(shader, source); // 编译着色器
gl.compileShader(shader); // 检查编译结果
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log("Failed to compile shader: " + error);
gl.deleteShader(shader);
return null;
} return shader;
}

缓冲区

创建好缓冲区对象后,需要把它分配给变量,然后使它生效。注意顶点数组使用的是类型化数组Float32Array,这样更加高效。vertexAttribPointer方法这里指定了每个顶点分量的个数为2,因为我们目前只定义x,y坐标,z坐标使用系统默认。

/**
* 创建缓冲区
* @param {Array} data
* @param {Object} bufferType
* @return {Object}
*/
function createBuffer(data, bufferType) {
// 生成缓存对象
var buffer = gl.createBuffer();
if (!buffer) {
console.log("Failed to create the buffer object");
return null;
}
// 绑定缓存(gl.ARRAY_BUFFER<顶点>||gl.ELEMENT_ARRAY_BUFFER<顶点索引>)
gl.bindBuffer(bufferType || gl.ARRAY_BUFFER, buffer); // 向缓存中写入数据
gl.bufferData(bufferType || gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); // 将绑定的缓存设为无效
// gl.bindBuffer(gl.ARRAY_BUFFER, null); // 返回生成的buffer
return buffer;
} // 创建缓冲区并传人顶点
var vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]);
if (!createBuffer(vertices)) return; // 分配缓冲区对象给a_Position变量
// (地址,每个顶点分量的个数<1-4>,数据类型<整形,符点等>,是否归一化,指定相邻两个顶点间字节数<默认0>,指定缓冲区对象偏移量<默认0>)
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); // 启动
gl.enableVertexAttribArray(a_Position);

写入数据

首先要获取变量的地址,然后再给变量赋值,感觉挺麻烦的。attribute标记的变量使用getAttribLocation获取,同理uniform标记的变量使用getUniformLocation获取。

我们的动画要使图形绕坐标原点旋转,那么这就需要用到矩阵的变换,矩阵相关的知识就不详细说明了。要注意webGL使用的是列主序的矩阵,计算好变换矩阵后,把值赋予变量就ok。

// 获取 u_FragColor变量的存储地址并赋值
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) return;
//颜色模式为rgba,值范围0~1
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0); // 绕z轴旋转
var deg=Math.PI/180*(angle++),
cos=Math.cos(deg),
sin=Math.sin(deg); // webgl中是按列主序 旋转加位移
var xformMatrix=new Float32Array([
cos,sin,0.0,0.0,
-sin,cos,0.0,0.0,
0.0,0.0,1.0,0.0,
0.3,0.0,0.0,1.0
]); // v表示可以向着色器传输多个数值(地址变量,webgl中必须false,矩阵)
gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);

背景操作

每次执行动画前进行清屏,和canvas中的设置fillStyle,执行clearRect,效果一样。

// 设置清屏颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清屏
gl.clear(gl.COLOR_BUFFER_BIT);

绘制

最后渲染图形,注意第一个参数,指定不同的值,它就渲染为不同的图形,大家可以用不同的值试试效果。

  • POINTS 点
  • LINES 线段
  • LINE_STRIP 线条
  • LINE_LOOP 回路
  • TRIANGLES 三角形
  • TRIANGLE_STRIP 三角带
  • TRIANGLE_FAN 三角扇
// (基本图形,第几个顶点,执行几次),修改基本图形项可以生成点,线,三角形,矩形,扇形等
gl.drawArrays(gl.TRIANGLES, 0, 3);

最后主体代码如下:

var canvas = document.getElementById("canvas"),
gl = get3DContext(canvas, true); function main() {
if (!gl) {
console.log("Failed to get the rendering context for WebGL");
return;
} if (!createShaders(gl, "fs", "vs")) {
console.log("Failed to intialize shaders.");
return;
} // 创建缓冲区并传人顶点
var vertices = new Float32Array([ -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5 ]);
if (!createBuffer(vertices)) {
console.log("Failed to create the buffer object");
return;
} // 获取顶点位置
var a_Position = gl.getAttribLocation(gl.program, "a_Position");
if (a_Position < 0) {
console.log("Failed to get the storage location of a_Position");
return;
} // 分配缓冲区对象给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position); // 获取 u_FragColor变量的存储地址并赋值
var u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");
if (!u_FragColor) {
console.log("Failed to get the storage location of u_FragColor");
return;
}
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0); // 获取矩阵变量
var u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
if (!u_xformMatrix) {
console.log("Failed to get the storage location of u_xformMatrix");
return;
} var xformMatrix,
angle = 0;
// 设置清屏颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 执行动画
(function animate() {
var deg = (Math.PI / 180) * angle++,
cos = Math.cos(deg),
sin = Math.sin(deg); // 旋转加位移
xformMatrix = new Float32Array([
cos, sin, 0.0, 0.0,
-sin, cos, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.3, 0.0, 0.0, 1.0
]); // v表示可以向着色器传输多个数值(地址变量,webgl中必须false,矩阵)
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix); gl.clear(gl.COLOR_BUFFER_BIT); // (基本图形,第几个顶点,执行几次),修改基本图形项可以生成点,线,三角形,矩形,扇形等
gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(animate);
})();
} main();

总结

相比canvas,webGL的api要原始得多,涉及到很多底层的openGL细节,但经过封装后,我们可以把那部分细节看成一个黑箱。大部分的操作都是基于矩阵变换,尽管有很多方便的第三方矩阵库,但有牢固的线性代数基础还是大有裨益的,GLSL编程语言也是一样需要熟练掌握。

WebGL学习(1) - 三角形的更多相关文章

  1. WebGL学习(2) - 3D场景

    原文地址:WebGL学习(2) - 3D场景 经过前面WebGL学习(1) - 三角形的学习,我们已经掌握了webGL的基础知识,也已经能够画出最基本的图形,比如点,线,三角形,矩形等.有了2D绘图的 ...

  2. webgl学习笔记五-纹理

    写在前面 建议先阅读下前面我的三篇文章. webgl学习笔记一-绘图单点 webgl学习笔记二-绘图多点 webgl学习笔记三-平移旋转缩放 术语 : 纹理 :图像 图形装配区域 :顶点着色器顶点坐标 ...

  3. WebGL学习之法线贴图

    实际效果请看demo:纹理贴图 为了增加额外细节,提升真实感,我们使用了漫反射贴图和高光贴图,它们都是向三角形进行附加纹理.但是从光的视角来看是表面法线向量使表面被视为平坦光滑的表面.以光照算法的视角 ...

  4. WebGL学习笔记二——绘制基本图元

    webGL的基本图元点.线.三角形 gl.drawArrays(mode, first,count) first,代表从第几个点开始绘制即顶点的起始位置 count,代表绘制的点的数量. mode,代 ...

  5. WebGL学习(3) - 3D模型

      原文地址:WebGL学习(3) - 3D模型   相信很多人是以创建逼真酷炫的三维效果为目标而学习webGL的吧,首先我就是

  6. WebGL学习之纹理贴图

    为了使图形能获得接近于真实物体的材质效果,一般会使用贴图,贴图类型主要包括两种:漫反射贴图和镜面高光贴图.其中漫反射贴图可以同时实现漫反射光和环境光的效果. 实际效果请看demo:纹理贴图 2D纹理 ...

  7. webgl学习笔记四-动画

    写在前面 建议先阅读下前面我的三篇文章. webgl学习笔记一-绘图单点 webgl学习笔记二-绘图多点 webgl学习笔记三-平移旋转缩放   下面我们将讲解下如何让一个正方形动起来~不断擦除和重绘 ...

  8. webgl学习笔记三-平移旋转缩放

    写在前面 建议先阅读下前面我的两篇文章. webgl学习笔记一-绘图单点 webgl学习笔记二-绘图多点 平移 1.关键点说明 顶点着色器需要加上 uniform vec4 u_Translation ...

  9. webgl学习笔记二-绘图多点

    写在前面 建议先看下第一篇webgl学习笔记一-绘图单点 第一篇文章,介绍了如何用webgl绘图一个点.接下来本文介绍的是如何绘制多个点.形成一个面. webgl提供了一种很方便的机制,即缓冲区对象, ...

随机推荐

  1. FPGA在其他领域的应用(二)

    计算机和存储领域: 计算机技术和存储技术发展迅猛.如今,云计算正在实现对传统 IT 功能和全新功能的整合.例如,许多大型数据中心目前正在同时提供传统的 IT 服务以及新型的数据分析服务. 因此,这些大 ...

  2. Python自学笔记-logging模块详解

    简单将日志打印到屏幕: import logging logging.debug('debug message') logging.info('info message') logging.warni ...

  3. IPSec协议

    IPSec协议:IPsec将IP数据包的内容先加密再传输,即便中途被截获,由于缺乏解密数据包所必要的密钥,攻击者也无法获取里面的内容. 传输模式和隧道模式:IPsec对数据进行加密的方式有两种:传输模 ...

  4. JS实现数组每次只显示5条数据

    var array = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]; //循环样式结构function fun(arr,index){ var str = &qu ...

  5. java自动化测试-http请求get

    首先我10.1过来自己玩通了讨鬼转极,看了电视剧白夜追凶,换了工作小组,这段时间确实比较少的更新博客,确实有点不勤奋,我先自我检讨 我就不赘述java的安装了,这个是比较简单的,有必要的话以后在讲 对 ...

  6. 在 Tomcat 8 部署多端口项目

    一般的部署途径 Tomcat 的部署途径很多,一般有如下几种: 直接将 War 包拷贝到 webapps 目录中,然后启动 Tomcat. 登陆 Tomcat 管理控制台http://localhos ...

  7. win10 uwp 绑定静态属性

    Jasoon 大神问,如何绑定静态属性. 我们经常有静态属性,那么我们如何绑定 假如我们的ViewModel有一个静态属性 public static string CVTE { set; get; ...

  8. 微信开发-微信JSSDK错误:invalid url domain

    错误类型:invalid url domain 调试返回参数: { "errMsg": "config:invalid url domain" } 截图: 环境 ...

  9. SpringMVC 基本概念

    DispatcherServlet:前端控制器,解释用户请求,通过HandlerMapping查找对应Handler处理请求,调用ViewResolve回填页面,DispatcherServlet在W ...

  10. 如何把项目上传到GitHub上

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 15.0px Consolas; color: #a5b2b9 } span.Apple-tab-span ...