「进校 · 分享」浅入深出Three.js

699 阅读7分钟

了解他

Three.js

  • 简介

Three.js是基于原生WebGL封装运行的三维引擎,在所有WebGL引擎中,Three.js是国内文资料最多、使用最广泛的三维引擎。

  • 业务场景

主要用于可视化方向,常见的业务场景有webVR、3D演示、3D游戏开发等。

  • 官方案例

WebGL

  • What

WebGL(Web 图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与** **OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5 <canvas>元素中使用。 这种一致性使 API 可以利用用户设备提供的硬件图形加速。

我们可以将其理解为一种帮助我们开发 3D 网页的绘图技术,还是一种 JavaScript API,它可以借助系统显卡来在浏览器里更流畅地展示 3D 场景和模型。

  • 兼容性

image

OpenGL

  • what

Open Graphics Library,译名:“开放图形库”或者“开放式图形库”,是用于渲染2D、3D矢量图形的跨语言、跨平台的****应用程序编程接口(API,其操作在GPU上,可以实现硬件加速渲染。这个接口由近350个不同的函数调用组成,用来绘制从简单的图形比特到复杂的三维景象。

OpenGL用于桌面系统,在移动平台上的是其嵌入式的版本,叫做OpenGL ES(OpenGL Embedded Systems)。OpenGL ES 1.0把三维带进了移动平台,2.0取代了大多数旧的API,替换为新的可编程API;2012年8月,Khronos组织确定了3.0的规范,并向后兼容了OpenGL ES 2.0。

  • CPU VS GPU

**CPU **就像个大的工业管道,等待处理的任务只能依次的通过这跟管道,所以CPU处理这些任务的速度完全取决于处理单个任务的时间。

image

CPU处理单个任务的能力十分的强大,这样的特性让CPU适合处理一些单个大型任务,但是处理图像,一般是数量庞大且不复杂,所以CPU并不适合图形处理,这时候,GPU登场了。

image

GPU 是由大量的小型处理单元构成的,它可能远远没有 CPU 那么强大,但胜在数量众多,可以保证同一时间下,每个单元都可以处理一个简单的任务,且互不阻塞。

我们可以可以这样理解,OpenGL提供了我们可以利用GPU加速渲染的底层能力,并开放了API,可供WebGL使用。

总结一下

流程图.jpg

那么,观众朋友可能会问了,WebGL就已经是前端同学可使用的js api,为啥还出现了three.js呢,让我们下一话接着看。

理解他

为什么不直接用WebGL

  1. 在JavaScript里直接使用webGL编程,创建三维场景并生成动画,这个过程非常复杂,而且容易出错,three.js库可以简化这个过程。

  2. 一般情况下,无论是简单还是复杂的3D场景,three.js都可以充分的实现我们的需求,几乎用不到底层WebGL。当然,学习WebGL知识可以让我们更加深入的理解渲染原理。

让我们用事实说话,举个简单的🌰,3D立方体

  • 使用WebGL绘制
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>webgl</title> </head> <body onload="init()"> <canvas id="canvas" height="800" width="1200"></canvas> </body> <!-- 顶点着色器 --> <script id="vertexShader" type="x-shader/x-vertex"> //浮点数设置为中等精度 precision mediump float; attribute vec4 apos; attribute vec4 acolor; varying vec4 vcolor; //矩阵变量 uniform mat4 mx; //矩阵变量 uniform mat4 my; void main() { //两个旋转矩阵、顶点齐次坐标连乘 gl_Position = mx*my*apos; vcolor=acolor; } </script> <!-- 片元着色器 --> <script id="fragmentShader" type="x-shader/x-fragment"> // 所有float类型数据的精度是lowp precision mediump float; varying vec4 vcolor; void main() { gl_FragColor =vcolor; } </script> <script> function init() { //通过getElementById()方法获取canvas画布 const canvas = document.getElementById('canvas'); //通过方法getContext()获取WebGL上下文 const gl = canvas.getContext('webgl'); //顶点着色器源码 const vertexShaderSource = document.getElementById('vertexShader').innerText; //片元着色器源码 const fragShaderSource = document.getElementById('fragmentShader').innerText; //初始化着色器 const program = initShader(gl, vertexShaderSource, fragShaderSource); initBuffer(gl, program); render(gl, program); } // 声明初始化着色器函数 function initShader(gl, vertexShaderSource, fragmentShaderSource) { const vertexShader = gl.createShader(gl.VERTEX_SHADER); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(vertexShader, vertexShaderSource); gl.shaderSource(fragmentShader, fragmentShaderSource); gl.compileShader(vertexShader); gl.compileShader(fragmentShader); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); return program; } //着色器变量获取及赋值 function initBuffer(gl, program) { //获取顶点着色器的位置变量apos const aposLocation = gl.getAttribLocation(program, 'apos'); const acolor = gl.getAttribLocation(program, 'acolor'); //创建立方体的顶点坐标数据 const data = new Float32Array([ -0.5, -0.5, 0.5, 1, 0, 0, 1, 0.5, -0.5, 0.5, 1, 0, 0, 1, 0.5, 0.5, 0.5, 1, 0, 0, 1, -0.5, 0.5, 0.5, 1, 0, 0, 1, -0.5, 0.5, 0.5, 0, 1, 0, 1, -0.5, 0.5, -0.5, 0, 1, 0, 1, -0.5, -0.5, -0.5, 0, 1, 0, 1, -0.5, -0.5, 0.5, 0, 1, 0, 1, 0.5, 0.5, 0.5, 0, 0, 1, 1, 0.5, -0.5, 0.5, 0, 0, 1, 1, 0.5, -0.5, -0.5, 0, 0, 1, 1, 0.5, 0.5, -0.5, 0, 0, 1, 1, 0.5, 0.5, -0.5, 1, 0, 1, 1, 0.5, -0.5, -0.5, 1, 0, 1, 1, -0.5, -0.5, -0.5, 1, 0, 1, 1, -0.5, 0.5, -0.5, 1, 0, 1, 1, -0.5, 0.5, 0.5, 1, 1, 0, 1, 0.5, 0.5, 0.5, 1, 1, 0, 1, 0.5, 0.5, -0.5, 1, 1, 0, 1, -0.5, 0.5, -0.5, 1, 1, 0, 1, -0.5, -0.5, 0.5, 0, 1, 1, 1, -0.5, -0.5, -0.5, 0, 1, 1, 1, 0.5, -0.5, -0.5, 0, 1, 1, 1, 0.5, -0.5, 0.5, 0, 1, 1, 1, ]); //顶点索引数据构造 const indexdata = new Uint16Array([ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 ]) vertexBuffer(gl,data,aposLocation,3,'',4*7,0); vertexBuffer(gl,indexdata,acolor,4,'index',4*7,12); render(gl, program, indexdata.length) } //判断是否是顶点索引还是常规方式,并创建缓冲区 function vertexBuffer(gl,data,position,n,type,rowCount=0,offset=0){ //创建缓冲区对象 const buffer = gl.createBuffer(); if(type==='index'){ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW); }else{ //绑定缓冲区对象 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); //顶点数组data数据传入缓冲区 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); } ///缓冲区中的数据按照一定的规律传递给位置变量apos gl.vertexAttribPointer(position, n, gl.FLOAT, false, rowCount, offset); //允许数据传递 gl.enableVertexAttribArray(position); } function render(gl, program, count=36) { tranlate(gl, program); //设置清屏颜色为黑色。 gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, 0); //调用requestAnimationFrame动画函数,使立方体动起来 // requestAnimationFrame(()=>{ // render(gl, program,count); // }) } let angle=30.0; function tranlate(gl, program) { const rad = angle * Math.PI / 180; const cos = Math.cos(rad); const sin = Math.sin(rad); const mx = gl.getUniformLocation(program, 'mx'); const mxArr = new Float32Array([ 1, 0, 0, 0, 0, cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1 ]) gl.uniformMatrix4fv(mx, false, mxArr); const my = gl.getUniformLocation(program, 'my'); const myArr = new Float32Array([ cos, 0, -sin, 0, 0, 1, 0, 0, sin, 0, cos, 0, 0, 0, 0, 1 ]) gl.uniformMatrix4fv(my, false, myArr); //每次渲染时,沿x轴和y轴旋转1度 angle+=1; } </script> </html>

image

  • 使用three.js
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>three.js</title> <style> body { margin: 0; overflow: hidden; /* 隐藏body窗口区域滚动条 */ } </style> <!--引入three.js三维引擎--> <script src="http://www.yanhuangxueyuan.com/threejs/build/three.js"></script> </head> <body> <script> /** * 创建场景对象Scene */ var scene = new THREE.Scene(); /** * 创建网格模型 */ // var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象 var geometry = new THREE.BoxGeometry(100, 100, 100); //创建一个立方体几何对象Geometry var material = new THREE.MeshLambertMaterial({ color: 0x0000ff }); //材质对象Material var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh scene.add(mesh); //网格模型添加到场景中 /** * 光源设置 */ //点光源 var point = new THREE.PointLight(0xffffff); point.position.set(400, 200, 300); //点光源位置 scene.add(point); //点光源添加到场景中 //环境光 var ambient = new THREE.AmbientLight(0x444444); scene.add(ambient); /** * 相机设置 */ var width = window.innerWidth; //窗口宽度 var height = window.innerHeight; //窗口高度 var k = width / height; //窗口宽高比 var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大 //创建相机对象 var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000); camera.position.set(200, 300, 200); //设置相机位置 camera.lookAt(scene.position); //设置相机方向(指向的场景对象) /** * 创建渲染器对象 */ var renderer = new THREE.WebGLRenderer(); renderer.setSize(width, height);//设置渲染区域尺寸 renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色 document.body.appendChild(renderer.domElement); //body元素中插入canvas对象 //执行渲染操作 指定场景、相机作为参数 renderer.render(scene, camera); </script> </body> </html>

image

尽管从功能上而言原生 WebGL API 可以满足我们任意场景的开发需要,但是,其开发和学习的成本极其昂贵。对于 WebGL 的初学者而言是极度不友好的,我们需要配置顶点着色器用于计算绘制顶点所在的位置,而这对于开发者而言需要一定的数学基础熟悉矩阵的运算,同时也要有空间几何的概念熟悉 3D 物体的空间分布,另外,还有纹理、写入显存、自定义几何体等台阶。

由此看来,three.js较低的学习成本、高级的封装方法、更加友好的api,无疑可以大大提高我们的开发效率。

Three.js为我们做了什么

  • WebGL工作流程

1280X1280.png

  1. 准备数据阶段 在这个阶段,我们需要提供顶点坐标、索引(三角形绘制顺序)、uv(决定贴图坐标)、法线(决定光照效果),以及各种矩阵(比如投影矩阵)。 其中顶点数据存储在缓存区(因为数量巨大),以修饰符attribute传递给顶点着色器,矩阵则以修饰符uniform传递给顶点着色器。

  2. 生成顶点着色器 根据我们需要,由Javascript定义一段顶点着色器(opengl es)程序的字符串,生成并且编译成一段着色器程序传递给GPU。

  3. 图元装配 GPU根据顶点数量,挨个执行顶点着色器程序,生成顶点最终的坐标,完成坐标转换。

  4. 生成片元着色器 模型是什么颜色,看起来是什么质地,光照效果,阴影(流程较复杂,需要先渲染到纹理,可以先不关注),都在这个阶段处理。

  5. 光栅化 能过片元着色器,我们确定好了每个片元的颜色,以及根据深度缓存区判断哪些片元被挡住了,不需要渲染,最终将片元信息存储到颜色缓存区,最终完成整个渲染。

  • Three.js工作流程

image

黄色绿色部分,都是three.js参与的部分,其中黄色是javascript部分,绿色是opengl es部分。 我们发现,能做的,three.js基本上都帮我们做了。

  • 辅助我们导出了模型数据;

  • 自动生成了各种矩阵;

  • 生成了顶点着色器;

  • 辅助我们生成材质,配置灯光;

  • 根据我们设置的材质生成了片元着色器。 而且将webGL基于光栅化的2D API,封装成了我们人类能看懂的 3D API。

Just Do IT

image

接下来,我们尝试用Three.js简单搭建一个3D旋转的正方形。(使用框架为React)

安装

// 安装three包
yarn add three -S

使用typescipt需要安装@types/three

基础设施搭建(创建场景)

可以把自己想象成一个摄影师,那么摄影必须的元素是啥呢,没错,就是场景 + 摄像机 + 画面成像(渲染器)

  • 场景:能够让你在什么地方、摆放什么东西来交给three.js来渲染,这是你放置物体、灯光和摄像机的地方

  • 摄像机:three.js有多种相机,这次我们用的是透视摄像机,它通过:视野角度(FOV)、长宽比(aspect ratio)、近截面(near)和远截面(far),四个参数进行设置。

  • 渲染器:

// 创建场景
const scene = new THREE.Scene();
// 创建透视摄像机
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
// 创建渲染器
const renderer = new THREE.WebGLRenderer();

创建拍摄实例

// 创建一个立方体
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
// 创建一个材质
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
// 创建一个网格,将立方体和材质结合,是场景直接使用的对象
const cube = new THREE.Mesh( geometry, material );
// 将网格添加到场景中
scene.add( cube );
// 调整摄像机高度
camera.position.z = 5;

调用scene.add()的时候,物体将会被添加到(0,0,0)坐标,使得摄像机和立方体彼此在一起。为了防止这种情况的发生,我们需要将摄像机稍微向外移动一些。

设置3D旋转动画

没啥好说的,不想成为建筑师的摄影师不是好动效师。让我们为动画的每一帧设置变化。

...
// 3D旋转动画
const animate = () => {
    // 按帧执行动画
    requestAnimationFrame(animate);
    // 修改旋转角度
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    cube.rotation.z += 0.01;
    // 更新场景喝摄像机
    renderer.render(scene, camera);
};

完整代码

酱,一个简单的3D旋转立方体就完成了,意不意外,开不开心

其他

参考文档

blog.csdn.net/Scott\_S/ar…

juejin.cn/post/689560…

juejin.cn/post/699494…

www.pianshen.com/article/662…