前端3D应用开发—— ThreeJS入门与3D应用开发简介

前言

计算机 3D 图形的历史可以追溯到 20 世纪 60 年代,几乎和计算机本身的历史一样长。它被广泛应用于工程、教育、培训、建筑、金融、销售、市场、博彩、娱乐等各个领域。曾经 ,3D 图形只能用计算机软件渲染。如今,所有的计算机和移动设备都搭载了 3D 图形处理硬件,普通智能手机甚至有着比五年前的专业图形工作站更为优秀的图形处理能力。更重要的是现代 Web 浏览器也支持了 3D 渲染,相比昂贵的 3D专用渲染 件,浏览器显然更普遍,更易于获取,并且是免费的。

HTML5新型视觉媒介的核心是一系列先进的图形技术:WebGL、css3 3d,canvas。在接触WebGL领域的时候,积累了不少学习资料,分别从前端可视化、游戏、3D应用等不同方向展示WebGL的具体应用。他们有的自底向上,从计算机图形学开始到具体需求实现;有的自顶向下,从需求出发,一步步实现一个完整的应用。无论哪种方式,这一领域都需要漫长的耕耘,无法满足快速起步的目的。

目前这一系列主标题是前端3D应用开发,主要探讨如何从0到1落地一个3D项目。由于发现介绍原始的WebGL开发难度过大,内容枯燥,使用中心向两端的方式,根据实际情况向前或向后灵活展开,更为合适。当前安排如下:

  • 介绍Three.js及快速入门案例
  • Three.js简介

ThreeJS入门

以下内容整理自《Three.js开发指南》

获取源码的方式

1
git clone https://github.com/josdirksen/learning-threejs-third

代码需要运行在Web服务器,可以自行处理,也准备了一个在线资源

第一行代码

查看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// once everything is loaded, we run our Three.js stuff.
function init() {

// create a scene, that will hold all our elements such as objects, cameras and lights.
var scene = new THREE.Scene();

// create a camera, which defines where we're looking at.
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

// create a render and set the size
var renderer = new THREE.WebGLRenderer();
renderer.setClearColorHex();
renderer.setClearColor(new THREE.Color(0xEEEEEE));
renderer.setSize(window.innerWidth, window.innerHeight);

// show axes in the screen
var axes = new THREE.AxisHelper(20);
scene.add(axes);

// create the ground plane
var planeGeometry = new THREE.PlaneGeometry(60, 20);
var planeMaterial = new THREE.MeshBasicMaterial({color: 0xcccccc});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);

// rotate and position the plane
plane.rotation.x = -0.5 * Math.PI;
plane.position.x = 15;
plane.position.y = 0;
plane.position.z = 0;

// add the plane to the scene
scene.add(plane);

// create a cube
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
var cubeMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true});
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

// position the cube
cube.position.x = -4;
cube.position.y = 3;
cube.position.z = 0;

// add the cube to the scene
scene.add(cube);

// create a sphere
var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
var sphereMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff, wireframe: true});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

// position the sphere
sphere.position.x = 20;
sphere.position.y = 4;
sphere.position.z = 2;

// add the sphere to the scene
scene.add(sphere);

// position and point the camera to the center of the scene
camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 30;
camera.lookAt(scene.position);

// add the output of the renderer to the html element
document.getElementById("WebGL-output").appendChild(renderer.domElement);

// render the scene
renderer.render(scene, camera);
}
window.onload = init;

代码中首先定义了场景、摄像机和渲染器对象。场景是一个容器,示例中的立方体和圆球都会添加到场景中;摄像机决定了能够在场景中看到什么;渲染器会基于摄像机角度来计算渲染成什么样子。后面我们有机会了解这些概念是怎么一回事

第二部分添加了轴和平面。我们创建了坐标轴对象并设置粗细20,最后调用scene.add方法将轴添加到场景。平面创建分两步进行,首先定义大小,然后通过创建材质来设置外观,然后将大小和外观组合进Mesh对象并赋予给平面变量。材质的概念也是稍后要介绍的

以同样的方式加入方块和球,设置wireframe: true物体不会渲染为实体。最后一部分设置相机位置和lookAt方向。接下去还会使用光照、阴影、材质和动画来美化这个场景

添加材质、光源和阴影

基础材质不会对光源有任何反应,需要改为MeshLambertMaterial

1
2
3
4
5
var planeGeometry = new THREE.PlaneGeometry(60, 20);
var planeMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.receiveShadow = true;

1
2
3
4
5
6
// add spotlight for the shadows
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-40, 60, -10);
spotLight.castShadow = true;
scene.add(spotLight);

因为渲染阴影比较耗费性能,所以默认关闭,需要做如下修改:

1
2
3
4
5
 // create a render and set the size
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMapEnabled = true;

此外还需要明确指定哪个物体投射阴影,哪个接受阴影。

1
2
3
4
5
6
plane.receiveShadow = true;
//...
cube.castShadow = true;
//...
sphere.castShadow = true;

点击查看源码

Three.js简介

Three.js的作者Ricardo Cabello Miguel或者称呼为Mr.doob。Three.js一开始是用ActionScript编写的,2010年第一个使用SVG 和2DCanvas的版本诞生,几个月后WebGL发布了,便开始移植到 WebGL 上进行继续开发,从那时开始,Three.js逐渐强大 完善,并最终成为创建WebGL 3D 应用的最流行选择。

  • Three.js 隐藏了 WebGL 渲染中的底层细节。Three.js 简化了 WebGL API 的细节 将
    3D 场景表示为网格、材质、灯光(即图形开发者常用的各种对象类型)。

  • Three.js 非常强大。Three.js 不仅仅是 WebGL 的一个封装,它内置了许多可用千游戏开发、动画、演示、数据可视化、建模工具应用的非常有用的模型对象,还提供了用千特殊效果的后渲染机制。除了它本身的能力,Three.js 还拥有丰富的示例,你可以在你的项目中使用这些示例的代码。

  • Three.js 非常易用。Three.js 的 API 基千友好和易学的理念设计。随库提供的许多示例能够帮助你更好地入门。

  • Three.js 运行速度很快。Three.js 采用了 3D 图形的最佳大践,既保证了高性能,又不因此牺牲可用性。

  • Three.js 非常稳定。它包含完备的错误检查、异常和控制台警告,让开发者可以准确地跟踪程序问题。

  • Three.js 支持交互。WebGL 不具备原生的选中支持,这意味着你无法获取到鼠标经过了当前场景中的哪个物体。Three.js 提供了对选中的支持,使得为应用添加交互变得更加简单。

  • Three.js 支持数学计算。Three.js 包含强大易用的 3D 数学运算对象,如矩阵、映射和向匿。

  • Three.js 内置第三方文件格式支持。你可以利用 Three.js 提供的接口载入以文本格式存储的、用流行的建模工具导出的 3D 模型。Three.js 也定义了自己专属的 JSON 和二进制 3D 模型文件格式。

  • Three.js 是面向对象的。开发者基千设计优良的 JavaScript 对象编写代码,而不是仅仅去调用一些函数。

  • Three.js 是可扩展的。无论你希望为 Three.js 添加一些特性,还是定义自己的个性化
    Three.js,都是非常简单的事。如果现有的数据格式支持无法满足你的需求,那么你可以为特定的数据格式编写相应的插件。

  • Three.js 包含 2D Canvas、SVG、CSS 的渲染引擎。尽管 WebGL 已经非常流行,但它并未被所有的浏览器支持,这表示对某些应用来说,WebGL 并不是最好的选择。幸好 Three.js 还可以使用 2D Canvas 和 SVG 元素来渲染大部分的内容。当运行环境不支持 3D Canvas 的时候,这为你的代码提供了一个优雅的降级方案。

需要提醒大家的是,Three.js 也有一些不擅长的事情。比方说,Three.js 并不是一个游戏引擎。它缺乏一些游戏引擎所需的常用特性,如公告牌、头像、有限状态机以及物理引擎。 Three.js 也没有编写多人联机游戏所需的内置网络支持。你需要基千 Three.js 自行构建它,或是整合其他专用的代码库。同时 Three.js 也不是一个应用框架,它不具备一个框架应有的安装、卸载、事件处理和运行循环机制。

另外一个简单入门案例

WebGL入门

WebGL原生API是一种非常低等级的接口,就好像一个只有加法的计算器,尽管有许多诸如Three.js之类的图形库,如果想要掌控WebGL编程,依然需要坚持学习更多底层知识,因此自底向上的学习方式,可能会让你经历他人突飞猛进而自己仍然在画三角形的挫败感。不要放弃学习基础知识,也要灵活尝试成熟的框架。

WebGL的优势体现在Web和3D

什么是3D

3D 计算机图形(相对 2D 计算机图形而言)是使用三个维度来表示几何数据(通常使用笛卡儿坐标系)并将其存储在计算机中,用于计算和绘制成屏幕上 2D 图像的一类图形。这类图形可被存储起来随时浏览,也可用于实时显示。

WebGL坐标系统

默认情况下WebGL使用右手坐标系。

右手坐标系

WebGL对于使用左手还是右手坐标系这个问题是中立的,那为什么那么多书籍教程都将WebGL坐标系描述为右手呢?这是因为使用右手坐标系是个传统,当你开发自己的程序需要先确定使用的坐标系统,然后不再改变。这一点对于各类图形库同样成立,早期图形库大部分采用了右手坐标,时至今日右手坐标系已经成为了传统,以致成了GL图形语言的一部分。

那为什么会有疑问?如果所有人都接受同一个传统,就没问题了,但在特定场景下需要让WebGL选择一种坐标系完成运算,就需要知道它的默认设定,而默认设定并不总是右手坐标系!关于WebGL默认行为探索,比如隐藏面消除和裁剪坐标系统,可以参考**《WebGL编程指南》**附录D,目前,你可以认为WebGL就是右手坐标系。

变换矩阵

对于简单的变换,可以使用数学表达式来实现。但当情况变得复杂,你会发现表达式运算实际相当繁琐。比如旋转后平移,就需要两个表达式叠加,获得一个新等式,然后在顶点着色器中实现。也就是说每次要实现一个新的着色器,这很不科学。我们可以使用变换矩阵来完成这项工作。

向量的点乘和叉乘

相关资料B站

线性变换的本质

将矩阵和矢量相乘就获得一个新的矢量

[xyz]=[abcdefghi]×[xyz]\left[\begin{matrix} x^{'}\\y^{'}\\z^{'} \end{matrix} \right]=\left[ \begin{matrix} a & b & c\\ d & e&f\\ g & h & i \end{matrix} \right] \times \left[\begin{matrix} x\\y\\z \end{matrix} \right]

注意只有矩阵的列数与矢量的行数相等时,才可以将两者相乘

x=ax+by=czx^{'}= ax+by=cz

y=dx+ey+fzy^{'}= dx+ey+fz

z=gx+hy+izz^{'}= gx+hy+iz

变换矩阵:旋转

x=r(cosαcosβsinαsinβ)x^{'}= r(cos \alpha cos \beta-sin \alpha sin\beta)

y=r(sinαcosβcosαsinβ)y^{'}= r(sin \alpha cos \beta-cos \alpha sin\beta)

将3.2代入消除,最终可得

x=xcosβysinβx^{'}=xcos\beta -y sin\beta

y=xsinβ+ycosβy^{'}=xsin\beta +y cos\beta

二维旋转矩阵直观的理解方式是:三个基向量i,j,k :[1,0,0],[0,1,0],[0,0,1],因为线性变换的本质是基向量的变化,在二维平面中z轴方向不发生变化,而新的基向量的坐标其实是在原始坐标系的投影,我们直接想象如果逆时针旋转90度,可以直接想出原来的x轴到了原来y轴位置,原来y轴到了原来-x轴位置。单位向量的坐标就是[0,1,0],[-1,0,0],[0,0,1],是不是和上面矩阵值一致呢

相机、透视、视口、投影

3D系统通常使用相机(camera)概念来描述用户查看渲染好场景的观察点。相机定义了用户相对于场景位置朝向,它具备现实中相机的属性,如视野的尺寸,它定义了透视(perspective,即远处物体看起来小)。相机通常用一对矩阵表示。第一个矩阵定义相机位置和方向,类似变换矩阵。第二个矩阵表示3D到2D绘制坐标转换,称为投影矩阵(projection matrix)。这些细节一般工具都封装好了,只需要对准、拍摄。

下图描述了相机、视口、透视的概念。眼睛代表相机位置,x轴表示相机指向,两个矩形代表近剪裁平面和远剪裁平面。两个平面间定义了3D空间子集的范围,通称视锥体或视见体。只有位于视锥体中的物体才会真正被渲染。

谈谈对透视矩阵的理解:矩阵是一系列坐标转换,一个3D坐标系中的物体,我们以上帝视角来看,是绝对的坐标,不管远近,然而,因为视觉上的需求,需要做“透视”,即远近大小看起来不一致(想象一下火车头和火车尾)。即将绝对坐标转换为视觉坐标。

WebGL应用示例

WebGL就是个绘图库,类似Canvas 2D那样,是另外一种Canvas。为了在页面中渲染WebGL,应当执行以下步骤:

  • 创建一个Canvas元素
  • 获取Canvas上下文
  • 初始化视口
  • 创建一个或多个待渲染数据的缓冲
  • 创建一个或多个定义定点缓冲到屏幕空间转化规则的矩阵
  • 创建实现绘制算法的着色器
  • 使用参数初始化着色器
  • 绘制

Canvas元素及绘图上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function initWebGL(canvas) {

var gl = null;
var msg = "Your browser does not support WebGL, " +
"or it is not enabled by default.";
try
{
gl = canvas.getContext("experimental-webgl");
}
catch (e)
{
msg = "Error creating WebGL Context!: " + e.toString();
}

if (!gl)
{
alert(msg);
throw new Error(msg);
}

return gl;
}

视口

当你从canvas获取WebGL绘图上下文是,需要定义一个绘制区域的矩形边界。在WebGL中,这个矩形边界被称为视口。

1
2
3
4
function initViewport(gl, canvas)
{
gl.viewport(0, 0, canvas.width, canvas.height);
}

缓冲、缓冲数组和类型化数组

WebGL基于图元(primitive)进行绘制。图元是指不同类型的基本几何图形。WebGL的图元包括三角形、线、点。三角形是最常用的,通常使用两种形式存储:以数组形式存储的三角形和三角形带。图元以数组形式存储数据,这个数组称为缓冲(buffer),待绘制的顶点数据在缓冲中被定义。
下面展示如何创建单位(1X1)正方形顶点缓冲,我们使用三角形带,一个三角形带定义了一组连续的三角形,前三个顶点表示第一个三角形,后续三角形都与它前一个共用两个顶点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Create the vertex data for a square to be drawn
function createSquare(gl) {
var vertexBuffer;
vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
var verts = [
.5, .5, 0.0,
-.5, .5, 0.0,
.5, -.5, 0.0,
-.5, -.5, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
var square = {buffer:vertexBuffer, vertSize:3, nVerts:4, primtype:gl.TRIANGLE_STRIP};
return square;
}

Float32Array是因为WebGL才被引入浏览器的,类型化数组可以普遍使用,访问速度更快,耗费的内存更小。

矩阵

在绘制之前,首先要创建一对矩阵。一个矩阵用于定义正方形在3D坐标系统中的位置(相对于相机),这个矩阵同时包含相机位置和模型位置,称为模型-视图矩阵。在例子中,沿着z轴负方向对正方形平移(当相机位置被移动,程序实际进行的处理是根据当前相机位置对整个场景进行平移)。第二个矩阵是投影矩阵,着色器使用它执行3D-2D坐标转换。示例中定义了一个45度视野的透视相机。
我们用glMatrix开源库来进行矩阵运算,现在我们放弃关心透视矩阵推导过程及背后的观察原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var projectionMatrix, modelViewMatrix;

function initMatrices(canvas)
{
// Create a model view matrix with object at 0, 0, -3.333
modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -3.333]);

// Create a project matrix with 45 degree field of view
projectionMatrix = mat4.create();
// perspective(out, fovy, aspect, near, far)
mat4.perspective(projectionMatrix, Math.PI / 4, canvas.width / canvas.height, 1, 10000);
}

透视矩阵推导

透视矩阵

着色器

一个着色器可以应用于多个对象,因此实际应用中,整个场景通常使用一个着色器,通过设置不同参数,在不同几何上复用。着色器通常有两部分:顶点着色器(vertex shader)和片元着色器(fragment shader)。顶点着色器负责将物体坐标转换为2D显示区坐标,片元着色器负责转换好的顶点最终颜色输出,基于颜色、纹理、光照、材质等数值输入。

相关推荐

canvas API

WebGL API

SVG

3Blue1BrownB站

华中科技大学-计算机图形学

奇舞团月影可视化课程源码

《交互式计算机图形学——基于WebGL的自顶向下方法(第七版)

本站收集在线示例

参考资料

跟月影学可视化

WebGL编程指南

Three.js开发指南

Introduction to Computer Graphics A Practical Learning Approach

Author: sumshare
Link: http://blog.sumshare.cn/2021/03/27/webgl01/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.