什么是webgl?

WebGL(全写Web Graphics Library)是一种3D绘图协议,这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。显然,WebGL技术标准免去了开发网页专用渲染插件的麻烦,可被用于创建具有复杂3D结构的网站页面,甚至可以用来设计3D网页游戏等等。
现在,我们知道了WebGL是一个底层的标准,在这些标准被定义之后,Chrome、Firefox之类的浏览器实现了这些标准。然后,程序员就能通过JavaScript代码,在网页上实现三维图形的渲染了。如果这对你来说还是太抽象,别着急,稍后我们会用具体的例子来说明。

什么是Three.js库

Three.js是一个实现可视化的核心组件。在 Three.js 中,操纵场景图的最主要类是Object3D类。所有场景图中的节点都有名字和ID,可以通过DOM获得。使用JQuery主要为了与 DOM 元素交互和事件处理。
《基于HTML5与 WebGL的机器人3D环境下的运动学仿真》

目前,比较流行的WebGL框架有Three.js、PhiloGL、Babylon.js、SceneJS等。每个框架都有自己的特点。其中,Three.js是一个轻量级的用于在浏览器中创建3D计算机图形图像应用程序的JavaScript库。本文选用Three.js来开发系统,主要是因为 Three.js是目前应用最广泛的 WebGL框架,其文档资料也是最丰富的,而且完全采用JavaScript编写而成 ,非常适用于三维网页的开发。 《基于WebGL的交互平台设计与实现》

框架层是对3D引擎的封装。主要由第三方类库:Three.js、Sim.js 和 Stats.js 组成。
a.Three.js 是对 WebGL API的封装,提供了场景、光照、纹理、材质、模型的生成、加载与转换、以及矩阵运算等强大功能。 b.Sim.js 是对 Three.js 的进一步封装,更加面向对象。提供了 Sim.Object、Sim.Publisher、Sim.App 等功能强大的类,提高了代码的重用性。 c.Stats 提供了画面质量统计监控功能。 《基于WebGL的网上虚拟太阳系漫游系统的设计与实现》

那么,Three.js究竟能用来干什么呢?

Three.js封装了底层的图形接口,使得程序员能够在无需掌握繁冗的图形学知识的情况下,也能用简单的代码实现三维场景的渲染。汽车游戏

WebGL vs. Three.js

image.png

  1. var renderer = new THREE.WebGLRenderer({
  2. canvas: document.getElementById('mainCanvas')
  3. });
  4. renderer.setClearColor(0x000000); // black
  5. var scene = new THREE.Scene();
  6. var camera = new THREE.PerspectiveCamera(45, 4 / 3, 1, 1000);
  7. camera.position.set(0, 0, 5);
  8. camera.lookAt(new THREE.Vector3(0, 0, 0));
  9. scene.add(camera);
  10. var material = new THREE.MeshBasicMaterial({
  11. color: 0xffffff // white
  12. });
  13. // plane
  14. var planeGeo = new THREE.PlaneGeometry(1.5, 1.5);
  15. var plane = new THREE.Mesh(planeGeo, material);
  16. plane.position.x = 1;
  17. scene.add(plane);
  18. // triangle
  19. var triGeo = new THREE.Geometry();
  20. triGeo.vertices = [new THREE.Vector3(0, -0.8, 0),
  21. new THREE.Vector3(-2, -0.8, 0), new THREE.Vector3(-1, 0.8, 0)];
  22. triGeo.faces.push(new THREE.Face3(0, 2, 1));
  23. var triangle = new THREE.Mesh(triGeo, material);
  24. scene.add(triangle);
  25. renderer.render(scene, camera);
  1. var gl;
  2. function initGL(canvas) {
  3. try {
  4. gl = canvas.getContext("experimental-webgl");
  5. gl.viewportWidth = canvas.width;
  6. gl.viewportHeight = canvas.height;
  7. } catch (e) {
  8. }
  9. if (!gl) {
  10. alert("Could not initialise WebGL, sorry :-(");
  11. }
  12. }
  13. function getShader(gl, id) {
  14. var shaderScript = document.getElementById(id);
  15. if (!shaderScript) {
  16. return null;
  17. }
  18. var str = "";
  19. var k = shaderScript.firstChild;
  20. while (k) {
  21. if (k.nodeType == 3) {
  22. str += k.textContent;
  23. }
  24. k = k.nextSibling;
  25. }
  26. var shader;
  27. if (shaderScript.type == "x-shader/x-fragment") {
  28. shader = gl.createShader(gl.FRAGMENT_SHADER);
  29. } else if (shaderScript.type == "x-shader/x-vertex") {
  30. shader = gl.createShader(gl.VERTEX_SHADER);
  31. } else {
  32. return null;
  33. }
  34. gl.shaderSource(shader, str);
  35. gl.compileShader(shader);
  36. if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
  37. alert(gl.getShaderInfoLog(shader));
  38. return null;
  39. }
  40. return shader;
  41. }
  42. var shaderProgram;
  43. function initShaders() {
  44. var fragmentShader = getShader(gl, "shader-fs");
  45. var vertexShader = getShader(gl, "shader-vs");
  46. shaderProgram = gl.createProgram();
  47. gl.attachShader(shaderProgram, vertexShader);
  48. gl.attachShader(shaderProgram, fragmentShader);
  49. gl.linkProgram(shaderProgram);
  50. if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
  51. alert("Could not initialise shaders");
  52. }
  53. gl.useProgram(shaderProgram);
  54. shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
  55. gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
  56. shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
  57. shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
  58. }
  59. var mvMatrix = mat4.create();
  60. var pMatrix = mat4.create();
  61. function setMatrixUniforms() {
  62. gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
  63. gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
  64. }
  65. var triangleVertexPositionBuffer;
  66. var squareVertexPositionBuffer;
  67. function initBuffers() {
  68. triangleVertexPositionBuffer = gl.createBuffer();
  69. gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
  70. var vertices = [
  71. 0.0, 1.0, 0.0,
  72. -1.0, -1.0, 0.0,
  73. 1.0, -1.0, 0.0
  74. ];
  75. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
  76. triangleVertexPositionBuffer.itemSize = 3;
  77. triangleVertexPositionBuffer.numItems = 3;
  78. squareVertexPositionBuffer = gl.createBuffer();
  79. gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
  80. vertices = [
  81. 1.0, 1.0, 0.0,
  82. -1.0, 1.0, 0.0,
  83. 1.0, -1.0, 0.0,
  84. -1.0, -1.0, 0.0
  85. ];
  86. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
  87. squareVertexPositionBuffer.itemSize = 3;
  88. squareVertexPositionBuffer.numItems = 4;
  89. }
  90. function drawScene() {
  91. gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
  92. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  93. mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
  94. mat4.identity(mvMatrix);
  95. mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
  96. gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
  97. gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
  98. setMatrixUniforms();
  99. gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
  100. mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
  101. gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
  102. gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
  103. setMatrixUniforms();
  104. gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
  105. }
  106. function webGLStart() {
  107. var canvas = document.getElementById("lesson01-canvas");
  108. initGL(canvas);
  109. initShaders();
  110. initBuffers();
  111. gl.clearColor(0.0, 0.0, 0.0, 1.0);
  112. gl.enable(gl.DEPTH_TEST);
  113. drawScene();
  114. }

从上面的代码我们不难发现,使用原生WebGL接口实现同样功能需要5倍多的代码量,而且很多代码对于没有图形学基础的程序员是很难看懂的。由这个例子我们可以看出,使用Three.js开发要比WebGL更快更高效。尤其对图形学知识不熟悉的程序员而言,使用Three.js能够降低学习成本,提高三维图形程序开发的效率

开始使用Three.js

HTML5canvas标签

HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。画布是一个矩形区域,您可以控制其每一像素。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。 —w3c

创建canvas元素、通过javascript绘制、线条绘制、圆形绘制、渐变背景、将图形放到画布。
image.png

Three.js 库(下载)

image.png
**

Three.js 三大组件介绍

Three.js中的场景是一个物体的容器,开发者可以将需要的角色放入场景中。
同时,角色自身也管理着其在场景中的位置。相机的作用就是面对场景,在场景中取一个合适的景,把它拍下来。渲染器的作用就是将相机拍摄下来的图片,放到浏览器中去显示
image.png

image.png

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title></title>
  5. <style>canvas { width: 100%; height: 100% }</style>
  6. <script src="js/three.js"></script>
  7. </head>
  8. <body>
  9. <script>
  10. var scene = new THREE.Scene();
  11. var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
  12. var renderer = new THREE.WebGLRenderer();
  13. renderer.setSize(window.innerWidth, window.innerHeight);
  14. document.body.appendChild(renderer.domElement);
  15. var geometry = new THREE.CubeGeometry(1,1,1);
  16. var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
  17. var cube = new THREE.Mesh(geometry, material); scene.add(cube);
  18. camera.position.z = 5;
  19. function render() {
  20. requestAnimationFrame(render);
  21. cube.rotation.x += 0.1;
  22. cube.rotation.y += 0.1;
  23. renderer.render(scene, camera);
  24. }
  25. render();
  26. </script>
  27. </body>
  28. </html>

相机

正交投影vs透视投影
举个简单的例子来说明正交投影与透视投影照相机的区别。使用透视投影照相机获得的结果是类似人眼在真实世界中看到的有“近大远小”的效果(如下图中的(a));而使用正交投影照相机获得的结果就像我们在数学几何学课上老师教我们画的效果,对于在三维空间内平行的线,投影到二维空间中也一定是平行的(如下图中的(b))。
image.png
那么,你的程序需要正交投影还是透视投影的照相机呢?
一般说来,对于制图、建模软件通常使用正交投影,这样不会因为投影而改变物体比例;而对于其他大多数应用,通常使用透视投影,因为这更接近人眼的观察效果。当然,照相机的选择并没有对错之分,你可以更具应用的特性,选择一个效果更佳的照相机。

Three.js基本元素

在计算机世界里,3D世界是由点组成,两个点能够组成一条直线,三个不在一条直线上的点就能够组成一个三角形面,无数三角形面就能够组成各种形状的物体,如下图:
image.png
我们通常把这种网格模型叫做Mesh模型。给物体贴上皮肤,或者专业点就叫做纹理,那么这个物体就活灵活现了。最后无数的物体就组成了我们的3D世界。

定义一个点

var point1 = new THREE.Vecotr3(4,8,9);

定义一条线

  1. var p1 = new THREE.Vector3( -100, 0, 100 );
  2. var p2 = new THREE.Vector3( 100, 0, -100 );
  3. geometry.vertices.push(p1);
  4. geometry.vertices.push(p2);
  5. geometry.colors.push( color1, color2 );
  6. var line = new THREE.Line( geometry, material, THREE.LinePieces );
  7. scene.add(line);
  1. 定义点P1、P2
  2. 加入几何体
  3. 上色
  4. 创建线条
  5. 加入场景

image.png

定义面

image.png

  1. for ( var i = 0; i <= 20; i ++ ) {
  2. var line = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0x000000, opacity: 0.2 } ) );
  3. line.position.z = ( i * 50 ) - 500;
  4. scene.add( line );
  5. var line = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0x000000, opacity: 0.2 } ) );
  6. line.position.x = ( i * 50 ) - 500;
  7. line.rotation.y = 90 * Math.PI / 180;
  8. scene.add( line );
  9. }

坐标系

默认情况下是右手坐标系
image.png

定义几何形状

这里,width是x方向上的长度;height是y方向上的长度;depth是z方向上的长度;
image.png

材质

image.png

  1. new THREE.MeshBasicMaterial({
  2. color: 0xffff00,
  3. opacity: 0.75
  4. });

动画

物体.mp4 (745.86KB)

  1. function draw() {
  2. mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
  3. renderer.render(scene, camera);
  4. }
  5. id = setInterval(draw, 20);

外部导入

使用Three.js创建常见几何体是十分方便的,但是对于人或者动物这样非常复杂的模型使用几何体组合就非常麻烦了。因此,Three.js允许用户导入由3ds Max等工具制作的三维模型,并添加到场景中。
图片 1.png
image.png

  1. var loader = new THREE.OBJLoader();
  2. loader.load('../lib/port.obj', function(obj) {
  3. mesh = obj; //储存到全局变量中
  4. scene.add(obj);
  5. });

材质导入

图片 1.png

  1. var loader = new THREE.OBJMTLLoader();
  2. loader.addEventListener('load', function(event) {
  3. var obj = event.content;
  4. mesh = obj;
  5. scene.add(obj);
  6. });
  7. loader.load('../lib/port.obj', '../lib/port.mtl');

QQ20200313-101630-HD.mp4 (7.79MB)

组合

image.png
这里以一个机器人三维模型来说下层级模型的概念,比如一整个机器人通过一个组对象Group表示,然后一条腿用一个组对象Group表示,一条腿假设包含大腿和小腿两个网格模型Mesh,大腿和小腿两个网格模型可以作为父对象腿Group的两个字对象,Group表示的两条腿又可以作为机器人Group的两个子对象,这样的话就构成了机器人——腿——大腿、小腿三个层级。
就像一颗树一样可以一直分叉,如果根对象机器人的位置变化,那么腿也会跟着变化。对于Threejs中一样,如果Mesh是Group的子对象,如果Group平移变化,Mesh的位置同样跟着父对象Group平移变化。演示

QQ20200313-102530-HD.mp4 (3.2MB)

  1. var cubeA = new THREE.Mesh( geometry, material );
  2. cubeA.position.set( 100, 100, 0 );
  3. var cubeB = new THREE.Mesh( geometry, material );
  4. cubeB.position.set( -100, -100, 0 );
  5. var group = new THREE.Group();
  6. group.add( cubeA );
  7. group.add( cubeB );
  8. scene.add( group );

交互

QQ20200313-103353-HD.mp4 (4.12MB) QQ20200313-103838-HD.mp4 (349.16KB)Raycaster
image.png
**我们一般都会设置三维场景的显示区域,如果,指明当前显示的2d坐标给THREE.Raycaster的话,它将生成一条从显示的起点到终点的一条射线。也就是说,我们再屏幕上点击了一个点,在three.js里面获取的则是一条直线。参考

  1. //声明射线和mouse变量
  2. var raycaster = new THREE.Raycaster();
  3. var mouse = new THREE.Vector2();
  4. //通过鼠标点击的位置计算出射线所需要的点的位置,以屏幕中心为原点,值的范围为-1到1.
  5. mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  6. mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  7. //根据在屏幕的二维位置以及相机的矩阵更新射线的位置
  8. raycaster.setFromCamera(mouse, camera);
  9. // 获取射线直线和所有模型相交的数组集合
  10. var intersects = raycaster.intersectObjects(scene.children, true); //增加第二个参数

总结

image.png


参考网站:

https://github.com/mrdoob/three.js
https://www.ituring.com.cn/book/miniarticle/58552
https://davidscottlyons.com/projects/threejs-intro/
http://www.hewebgl.com/
https://blog.csdn.net/qq_30100043/article/details/81483918