threejs入门与实践

相关技术简介

Canvas:

作为html元素存在.提供渲染上下文用于手动绘制需要展示的内容.

OpenGL

一套规范,定义了一系列操作和显示图形的API.显卡厂家通过驱动的方式,来将对应的API调用转化为GPU指令.

WebGL

一种3D绘图协议.浏览器支持后,可以通过JavaScript调用本地OpenGL接口,从而在浏览器提供硬件3D加速渲染.

https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext

ThreeJs

基于WebGL封装运行的三维引擎,封装了一些3D渲染需求中重要的工具方法与渲染循环.
http://www.webgl3d.cn/Three.js/

3D渲染常用概念

场景

虚拟的三维世界,包含需要绘制的所有元素和参与绘制的工具.

模型

待绘制的元素以模型存在于场景中.
每一个模型由几何体和材质组成.

相机

三维场景在二维的投影会受到视角影响.
通过相机位置和方向的定义,来确定最终渲染的结果.

渲染器

根据场景和相机,将二维结果渲染到浏览器canvas上.

代码实操

最简单的场景实现:

1
2
3
<div id="threejs_root" style="height: 100%;width: 100%">
<canvas id="cell_canvas" class="canvas"></canvas>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const root = document.getElementById('threejs_root')
const canvas = document.getElementById('cell_canvas')
//创建场景
var scene = new THREE.Scene();
//设置相机
var width = root.clientWidth; //窗口宽度
var height = root.clientHeight; //窗口高度
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 5000);
camera.position.set(0, 0, 500); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
//设置渲染器
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
//开始渲染
renderer.render(scene, camera);

增加坐标轴

1
2
3
const axes = new THREE.AxesHelper(20000)
axes.setColors('red', 'green', 'blue')
scene.add(axes)

支持鼠标联动

将鼠标拖动,滚动转化为相机数据,实时更新渲染结果.

1
const controls = new OrbitControls(camera, canvas);//创建控件对象

放置模型

简单的长方体.
1
2
3
4
5
const geometry = new THREE.BoxGeometry( 100, 200, 300 );//创建立方几何体
const material = new THREE.MeshLambertMaterial({//网格材质,与光照有反应,漫反射
color:0xff0000});//设置材质颜色
const cube = new THREE.Mesh( geometry, material );//生成网格模型
scene.add(cube);
加载现有的obj模型.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
loadObj(){
return new Promise((resolve,reject) => {
if(cellObj){
resolve(cellObj)
}else{
const loader = new OBJLoader();
//只能使用绝对路径,建议放在静态资源目录下
loader.load('/cell2.obj', function (obj) {
console.log("obj:", obj)
// 查看obj模型的几何体大小,方便后续摆放
// const box = new THREE.Box3()
// box.setFromObject(cellObj);
// const v3 = new THREE.Vector3()
// box.getSize(v3)
// console.log('查看返回的包围盒尺寸', v3);//实际尺寸:40,41,12
cellObj = obj.children[0]
resolve(cellObj)
})
}
})
},

设置光源

点光源:
1
2
3
4
5
6
const point = new THREE.PointLight(0xffffff);
point.position.set(100, 200, 0);
scene.add(point);
//辅助工具
const pointLightHelper=new THREE.PointLightHelper(point,10,0xff00ff);
scene.add(pointLightHelper);
平行光:
1
2
3
4
5
6
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(400, 200, 300);
scene.add(directionalLight);
//辅助工具
const directionalLightHelper=new THREE.DirectionalLightHelper(directionalLight,50,0x00ff00);
scene.add(directionalLightHelper);
环境光:用于上述光源照射不到的面.会与材质颜色做混合运算.
1
2
const ambient = new THREE.AmbientLight(0x666666);
scene.add(ambient);

点击事件

点击事件本质上是将相机位置和屏幕点击位置连起来画射线,穿过的模型就是被点击的物体.
ThreeJs提供了射线类Raycaster来完成这个功能.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const raycaster = new THREE.Raycaster();
//点击监听.
onMouseDown() {
//将屏幕坐标系转化为webGL中的世界坐标系(屏幕中心为原点,左下为-1/-1,右上为1/1).
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
//使用二维坐标和起点来更新射线.
raycaster.setFromCamera(mouse, this.camera);
//得到点击的几何体列表(按从近向远排列)
var raycasters = raycaster.intersectObjects(this.scene.children);
if (raycasters.length > 0) {
const object = raycasters[0].object
console.log('命中的第一个模型:', object)
}
},
简单的点击特效:发光外壳

使用效果组合器接管渲染器.

1
2
3
var renderPass = new RenderPass(scene, camera);
var composer = new EffectComposer(renderer);
composer.addPass(renderPass);

1
this.renderer.render(this.scene, this.camera);

替换为

1
this.composer.render(this.scene, this.camera);

在效果组合器中增加一个轮廓处理器.

1
2
3
4
5
6
7
var outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
outlinePass.visibleEdgeColor = new THREE.Color(1, 1, 1);
outlinePass.hiddenEdgeColor = new THREE.Color(1, 1, 1);
outlinePass.edgeThickness = 3.0;
outlinePass.pulsePeriod = 3 //闪烁
var composer = new EffectComposer(renderer);
composer.addPass(renderPass);

点击事件中,为轮廓处理器设置目标对象.

1
2
const object = raycasters[0].object
this.outlinePass.selectedObjects = [object];

增加2D弹窗

简单的2D弹窗通过单独的渲染器CSS2DRenderer配合CSS2DObject实现.

直接在canvas同级添加一个element,并和Render绑定.

1
2
3
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(root.clientWidth, root.clientHeight);
canvas.parentElement.appendChild(labelRenderer.domElement);

使用dom element创建CSS2DObject,像添加3D模型一样添加到场景中.

1
2
3
4
5
6
7
8
const div = labelLayout.cloneNode(true);
div.style.visibility = "visible";
div.childNodes[0].childNodes[0].textContent = item.vol + 'V\n#21';
const label = new CSS2DObject(div);
label.name = item.name
label.position.copy(copyObj.position);//这里直接使用对应3D模型的坐标.
label.translateZ(40)//沿Z轴上移,显示效果为悬浮在对应模型上方.
scene.add(label)

渲染时,将CSS2DRenderer一起执行即可.

1
this.labelRenderer.render(this.scene, this.camera);

动画效果的实现.

屏幕显示的内容本质上是镜头和场景共同作用的结果.
只需要以固定间隔修改两者之一,即可做出动画效果.

  • 简单的视角改变,可以用通过镜头的位移和角度实现.
  • 模型动画,需要单独修改场景甚至指定模型.

示例:
指定几个变量,在render方法中开启动画对应的镜头计算.

1
2
3
4
5
6
7
8
9
10
11
12
render() {
this.composer.render(this.scene, this.camera);
this.labelRenderer.render(this.scene, this.camera);
requestAnimationFrame(this.render);
if (animating1) {
this.calAnimatPositions1()
} else if (animating2) {
this.calAnimatPositions2()
}else if (animating3) {
this.calAnimatPositions3()
}
},

三段动画:

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
calAnimatPositions1() {
// camera.position.set(500, 5, 150); //起始位置
// camera.position.set(0, 5, 25); //目标位置
this.camera.position.x -= 2;
this.camera.position.z -= 1;
this.camera.lookAt(0, 0, 0);
if (this.camera.position.z < 25) {
this.camera.position.z = 25
}
if (this.camera.position.x <= 0) {
animating1 = false
animating2 = true
}
},
calAnimatPositions2() {
// camera.position.set(0, 5, 25); //起始位置
// camera.position.set(0, -300, 100); //目标位置
this.camera.position.y -= 2;
this.camera.position.z += 1;
this.camera.lookAt(0, 0, 0);
if (this.camera.position.z > 100) {
this.camera.position.z = 100
}
if (this.camera.position.y <= -300) {
animating2 = false
animating3 = true
}
},
calAnimatPositions3() {
// camera.position.set(0, -300, 100); //起始位置
// camera.position.set(0, 300, 100); //目标位置
this.camera.position.y += 2;
if (this.camera.position.y <= 0) {
this.camera.position.x += 2;
} else {
this.camera.position.x -= 2;
}
this.camera.lookAt(0, 0, 0);
if (this.camera.position.y >= 300) {
animating3 = false
}
},

总结

ThreeJS提供的功能还远不止于此.纹理/精灵/骨骼等高级应用可以完成更酷炫和实用的需求.
参考资料:
https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene
http://www.webgl3d.cn/Three.js/
https://sogrey.top/Three.js-start/start/#top


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!