前言
长期以来作为web开发者和用户,我们都习惯了呈现2D效果的页面和图像,但随着大数据、物联网、5G、VR等技术的兴起,许多行业和产品的需求开始转向寻求3D效果的呈现。世界正在悄然变化,在座各位若干年后旧时代的残党如果想要登上新时代的航船,阅读本文将会是你迈向港口最初的一小步
Web 3D技术能做什么
产品3D浏览
在Web 2D时代,网页上的产品展示往往是通过图片或者二维图形来体现,如果想3D展示一个产品,往往需要通过unity3D或ue4开发一个桌面应用,这样做往往很难随意传播,因为需要用户下载程序很麻烦。
如今使用Web 3D技术我们可以通过Web的方式展示产品的三维模型,一个超链接就可以随意传播。加上5G技术的持续推广,各种产品在线3D展示将会变得越来越普及,比如一家汽车公司的新款轿车可以在官网上在线预览,以及一些电商平台会通过3D模型取代2D图片。现在你朋友推荐推荐给你一款新产品,你会说发一张图片看看,也许将来你会说发来一个3D模型链接看看
数据可视化
大数据时代,Web 3D技术给海量数据的可视化提供了更加能发挥自由想象力的实现方案
科教领域
WebVR
对于现在比较火的VR、AR概念,Web 3D技术的出现,也是一个好消息,如果想预览一些VR内容,完全可以不下载一个VR相关的APP,通过3D引擎实现VR内容发布,然后用户直接通过微信等社交方式推广,直接打开VR内容链接就可以观看。VR与Web3D技术结合自然就衍生出来一个新的概念WebVR,也就是基于Web实现的VR内容。
And more!
物联网可视化、游戏、工业设计... ...一切能向3D世界扩展的用户场景
技术名词解释
OpenGL
OpenGL全称Open Graphics Library,即开放式图形库,它是一套跨语言的图形API规范,定义了一系列用来操作2D&3D图形和图像的函数,由Khronos组织制定并维护(OpenGL并非唯一的图形API规范,其它还有DirectX、Metal等)
英伟达等GPU的硬件开发商会提供满足OpenGL等规范的实现,即“显卡驱动”,它负责将OpenGL等规范定义的API命令翻译为GPU指令。也就是说通过调用各个厂商提供的驱动函数,就可以操作其GPU。从这个角度来看,也可以说OpenGL是GPU功能的调用规范
OpenGL ES
OpenGL ES (OpenGL for Embedded Systems,嵌入式OpenGL) 是OpenGL的子集,针对手机、Pad和游戏主机等嵌入式设备而设计,去除了许多不必要和性能较低的API接口。
GLSL
GLSL (OpenGL Shading Language,OpenGL着色语言)是一种特殊的有着类似于 C 语言的语法的OpenGL 着色语言,一般用于编写着色器程序,在显卡驱动中编译执行 。GLSL 不同于 JavaScript, 它是强类型语言,并且内置很多数学公式用于计算向量和矩阵
着色器实际上就是绘制东西到屏幕上的函数。着色器运行在 GPU 中,有两种类型: 顶点着色器 (Vertex Shader) 和片元着色器 (Fragment Shader). 前者是将形状转换到真实的 3D 绘制坐标中,后者是计算最终渲染的颜色和其他属性用的。
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的浏览器能够支持JS操作GPU从而进行2D&3D图形渲染
WebGL 程序包括用 JavaScript 写的控制代码,以及在GPU中执行的GLSL着色代码
Three.js
Three.js是基于原生WebGL封装的3D图形工具库,JS开发者通过它可以屏蔽OpenGL和WebGL的许多底层概念,通过较为浅显的API实现Web 3D渲染功能
WebGL工作原理
在OpenGL/OpenGL ES/WebGL中我们只能渲染绘制三种图元(图形单元):点、线、三角形。不过所有其他图形,从圆到球体、从立方体到复杂三维模型,都可以由一个个三角形组成
首先简单了解下OpenGL编程:在OpenGL/OpenGL ES中,开发者一般编写的是顶点着色器和片元着色器。下面是图形渲染管线
顶点着色器操作 3D 空间的坐标并且每个顶点都会调用一次这个函数。其目的是设置 gl_Position 等变量:
void main() {
// 这是一个特殊的全局内置变量,它是用来存储当前顶点的位置
gl_Position = makeCalculationsToHaveCoordinates;
}
这个 void main() 函数是定义全局gl_Position 变量的标准方式。所有在这个函数里面的代码都会被着色器执行。 如果将 3D 空间中的位置投射到 2D 屏幕上这些信息都会保存在计算结果的变量中。
在顶点着色器进行的业务处理有:
-
矩阵变换的计算
-
计算光照公式生成逐顶点颜色
-
生成/变换纹理坐标
片元 (或者纹理) 着色器在计算时定义了每像素的 RGBA 颜色,每个像素只调用一次片段着色器。这个着色器的作用是设置 gl_FragColor 变量,也是一个 GLSL 内置变量:
void main() {
gl_FragColor = makeCalculationsToHaveColor;
}
在片元着色器的业务处理有:
-
计算颜色
-
获取纹素
-
往像素点中填充颜色值 它可以用于图片/视频中每个像素的颜色填充。比如给视频添加滤镜,实际上就是将视频中每个图片的像素点颜色填充进行修改
回到WebGL,它是基于OpenGL ES的,所以具有类似的工作原理,其绘制过程包括以下三步:
1、获取顶点坐标 (顶点就是勾勒物体形状轮廓的一系列三维坐标点)
由于顶点数据往往成千上万,在获取到顶点坐标后,我们通常会将它存储在显存,即缓存区内,方便GPU更快读取
2、图元装配(即画出一个个三角形)
前面OpenGL工作原理那里提到过,顶点着色器的主要职能是进行顶点坐标转化(三维点投射到屏幕坐标)以及确定顶点颜色等
3、光栅化(生成片元,即一个个像素点)
流程总览:
Three.js使用入门
概述
Three.js的主要功能是渲染3D图像,它基于WebGL封装,隐藏和帮助自动处理了大量绘制细节,其API设计思维十分贴合现实场景,因此容易理解和使用。
在现实生活中我们要拍摄一张照片,必要的元素是:场景&相机。其中前者是由拍摄对象(人、动植物、建筑物等)和光照(阳光/灯光、亮度、角度)构成的;而后者则需要确定拍摄的视角位置,视线方向等因素。这些变量确定下来后按下快门,就得到一张照片最终的呈现效果
Three.js的思维亦是类似:要渲染一帧静态的3D图像,需要明确场景(模型+光照)+ 相机(位置、方向、投影方式),最后使用渲染器“按下快门”即可
另外,如果我们需要做3D动画,在静态图像基础上设置定时器,使得图像逐帧渐变渲染即可。再如果我们想要通过鼠标等对3D图形进行旋转缩放等操作,则引入Three.js的控件,它会监听相关事件并自动完成图形的视觉变化。
重要概念
个人认为顶点、几何体、材质、模型是初学Three.js时最容易造成错乱的基本概念,后面的Demo也会提到,所以先简单介绍一下:
顶点&几何体
顶点就是一系列的三维坐标,每个三维坐标代表一个点;
几何体其实就是顶点的集合,因为顶点可以勾勒出物体的几何轮廓。
Three.js中创建几何体主要有以下方式:
-
使用内置的几何体(立方体、球体等基本形状)
-
外部导入(使用3D建模软件的文件数据,以构建复杂形状)
-
自定义顶点坐标集合
材质
所谓材质,简单地说就是字面意思,呈现几何体表面效果(比如塑料材质、金属材质等)的设置。通过材质我们可以赋予一个物体特定的颜色、透明度、纹理等效果
模型
模型 = 几何体 + 材质,我理解可以认为是Three.js世界中真实存在的物体,不过模型分为若干种类(点模型、线模型、网格模型等),同样的几何体+材质,如果选择生成不同类型的模型,会得到不同的渲染效果,如下所示
材质和模型的对应关系
使用材质的时候,要注意材质和模型的对应关系,需要按正确的关系进行搭配
快速上手:可旋转缩放的简单3D图形
API可查找官方文档
我们可以通过script或npm的方式在项目中引入three.js,前者需要下载源码仓库或者找到可用的CDN,后者则普通地安装npm包即可:
$ npm i three
$ npm i -D @types/three
核心源码
import * as React from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import "./styles.css";
const { useEffect } = React;
const render3d = () => {
/**
* 创建场景对象Scene
*/
var scene = new THREE.Scene();
/**
* 创建并添加网格模型到场景
*/
// 立方体
var geometry1 = new THREE.BoxGeometry(100, 100, 100);
// 材质
var material1 = new THREE.MeshLambertMaterial({
color: 0x0000ff
});
// 网格模型
var mesh1 = new THREE.Mesh(geometry1, material1); //网格模型对象Mesh
scene.add(mesh1); //网格模型添加到场景中
// 球体
var geometry2 = new THREE.SphereGeometry(60, 40, 40);
// 材质
var material2 = new THREE.MeshPhongMaterial({
color: 0xffff00,
specular: 0x4488ee,
shininess: 12
});
// 网格模型
var mesh2 = new THREE.Mesh(geometry2, material2); //网格模型对象Mesh
mesh2.translateY(120); //球体网格模型沿Y轴正方向平移120
scene.add(mesh2);
/**
* 设置光源并添加到场景
*/
//点光源
var point = new THREE.PointLight(0x444444);
point.position.set(100, 100, 100); //点光源位置
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 = 300; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(250, 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对象
function render() {
renderer.render(scene, camera); //执行渲染操作
}
render();
var controls = new OrbitControls(camera, renderer.domElement); //创建控件对象
controls.addEventListener("change", render); //监听鼠标、键盘事件
};
export default function App() {
useEffect(() => {
render3d();
}, []);
return null;
}
Demo:3D粒子波浪
这个Demo相比快速上手的例子,特别的点在于:
-
采用了点模型来模拟粒子,而非网格模型
-
使用自定义着色器材质来得到点模型的球状效果
-
动画效果
import * as React from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import Stats from "three/examples/jsm/libs/stats.module.js";
import "./styles.css";
const { useEffect } = React;
const render3d = () => {
const SEPARATION = 100;
const AMOUNTX = 50;
const AMOUNTY = 50;
let container;
let camera: THREE.PerspectiveCamera;
let scene: THREE.Scene;
const stats = Stats();
const renderer = new THREE.WebGLRenderer({ antialias: true });
let particles: THREE.Points;
let count = 0;
let mouseX = 0;
let mouseY = 0;
let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;
function init() {
container = document.createElement("div");
document.body.appendChild(container);
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
1,
10000
);
camera.position.z = 1000;
scene = new THREE.Scene();
const numParticles = AMOUNTX * AMOUNTY;
const positions = new Float32Array(numParticles * 3);
const scales = new Float32Array(numParticles);
let i = 0;
let j = 0;
for (let ix = 0; ix < AMOUNTX; ix++) {
for (let iy = 0; iy < AMOUNTY; iy++) {
positions[i] = ix * SEPARATION - (AMOUNTX * SEPARATION) / 2; // x
positions[i + 1] = 0; // y
positions[i + 2] = iy * SEPARATION - (AMOUNTY * SEPARATION) / 2; // z
scales[j] = 1;
i += 3;
j++;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("scale", new THREE.BufferAttribute(scales, 1));
const vertexShader = `
attribute float scale;
void main() {
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_PointSize = scale * ( 300.0 / - mvPosition.z );
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
uniform vec3 color;
void main() {
if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;
gl_FragColor = vec4( color, 1.0 );
}
`;
const material = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(0x00ffff) }
},
vertexShader,
fragmentShader
});
particles = new THREE.Points(geometry, material);
scene.add(particles);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
container.appendChild(stats.dom);
container.style.touchAction = "none";
function onPointerMove(event: PointerEvent) {
if (event.isPrimary === false) return;
mouseX = event.clientX - windowHalfX;
mouseY = event.clientY - windowHalfY;
}
container.addEventListener("pointermove", onPointerMove);
//
function onWindowResize() {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener("resize", onWindowResize);
}
function render() {
camera.position.x += (mouseX - camera.position.x) * 0.05;
camera.position.y += (-mouseY - camera.position.y) * 0.05;
camera.lookAt(scene.position);
const positions = particles.geometry.attributes.position.array;
const scales = particles.geometry.attributes.scale.array;
let i = 0,
j = 0;
for (let ix = 0; ix < AMOUNTX; ix++) {
for (let iy = 0; iy < AMOUNTY; iy++) {
positions[i + 1] =
Math.sin((ix + count) * 0.3) * 50 + Math.sin((iy + count) * 0.5) * 50;
scales[j] =
(Math.sin((ix + count) * 0.3) + 1) * 20 +
(Math.sin((iy + count) * 0.5) + 1) * 20;
i += 3;
j++;
}
}
particles.geometry.attributes.position.needsUpdate = true;
particles.geometry.attributes.scale.needsUpdate = true;
renderer.render(scene, camera);
count += 0.1;
}
function animate() {
requestAnimationFrame(animate);
render();
stats.update();
}
init();
animate();
};
export default function App() {
useEffect(() => {
render3d();
}, []);
return null;
}
其它Three.js Demo:
参考文档
OpenGL是什么:www.jianshu.com/p/d6694ccc5…
WebGL & Three.js工作原理:mp.weixin.qq.com/s/X17M-OC\_…
顶点着色器&片元着色器:www.jianshu.com/p/1122b46a1…
GLSL 着色器:developer.mozilla.org/zh-CN/docs/…
Three.js零基础入门:www.yanhuangxueyuan.com/Three.js/
Three.js官方文档:threejs.org/docs/index.…