本文代码仍然使用 Svelte 作为脚手架。如果不了解 Svelte,推荐阅读 Svelte 下的 three.js 开发:基础框架 ,本文也是继续上文的内容。

光源与材质

在 Three.js 中添加新的材质和灯光非常简单。要出现阴影,则前提就是要有光源(这个应该不难理解)。

我们在 Svelte 下的 three.js 开发:基础框架 代码的基础上,添加一个光源:

1
2
3
4
5
6
7
8
9
// 光源
let spotLight = new THREE.SpotLight(0xFFFFFF);
spotLight.position.set(-40, 40, -15);
spotLight.castShadow = true;
spotLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
spotLight.shadow.camera.far = 130;
spotLight.shadow.camera.near = 40;

scene.add(spotLight);

在前文中,场景中的物体使用的材质是 MeshBasicMaterial,它不能和光源发生作用,只能指定渲染的颜色。因此需要改变该场景中的物体(平面、立方体、球体)的材质:

1
2
3
let planeMaterial = new THREE.MeshLambertMaterial//...
let cubeMaterial = new THREE.MeshLambertMaterial// ...
let sphereMaterial = new THREE.MeshLambertMaterial//...

需要把立方体和球体的线框模式(wireframe)去掉。

更换材质

阴影

没有阴影,上面的场景就很假,就会给人一种贴图感觉。由于我们已经添加了光源,也更换了物体的材质,那么只需要将阴影选项打开即可:

1
2
3
4
5
6
renderer.shadowMap.enabled = true;
// 接收立方体和球体的阴影
plane.receiveShadow = true;
// 投射阴影
cube.castShadow = true;
sphere.castShadow = true;

暂时忽略锯齿,还是能看到效果前面那个好一点点:

投射阴影

动画

如果要让这个场景变成动画,就需要在固定的间隔(帧率)下重新绘制场景。HTML5 以前通常只能用 setInterval,现在则可以使用 requestAnimationFrame

1
2
3
4
5
6
7
8
9
10
11
12
onMount(() => {
domElem.appendChild(renderer.domElement);

let frame;
function renderScene() {
frame = requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}

renderScene();
return () => cancelAnimationFrame(frame);
})

return () => cancelAnimationFrame(frame); 类似注销操作,也就是结束销毁动作。

stats.js

现在没有任何改变,因为还没有任何动画操作。为了直观的看到渲染信息,可以使用 Stats.js,这个 js 库和 three.js 是同一个作者。

1
2
yarn add stats.js
yarn add @types/stats.js --dev

使用这个库也很简单,引入 Stats

1
2
3
4
5
6
7
8
9
10
import Stats from 'stats.js';

// ...

function initStats(type: number) {
let stats = new Stats();
stats.showPanel(type)
domElem.appendChild(stats.dom);
return stats;
}

使用也非常简单:

1
2
3
4
5
6
7
8
9
10
11
onMount(() => {
domElem.appendChild(renderer.domElement);
let stats = initStats(0);

let frame;
function renderScene() {
stats.update();
frame = requestAnimationFrame(renderScene)
renderer.render(scene, camera);
}
// ...

为了让 Stats 的面板固定在元素的左上角,而不是视口的左上角。可以考虑修改定位。postion 默认是 static,也称为静态定位。要让 Stats 的面板根据 div 定位,首先就要修改它的定位:

在 App.svelte 中添加样式:

1
2
3
4
5
6
7
8
9
<main>
<div class="three-scene" bind:this={domElem}></div>
</main>

<style>
.three-scene {
position: relative;
}
</style>

为了将 Stats 相对于 div 偏移,可以在 initStats 中修改 Stats 的样式,将定位的默认值从 fixed 修改为 absolute

1
stats.dom.style.position = "absolute"

absolute 相对于最近的非 static 定位元素进行偏移,而 fixed 是相对于屏幕视口位置偏移。

可以看到,现在 Stats 的面板会固定在元素的左上角:
stats面板

立方体旋转

现在场景还是静态的,我们必须指定每一帧的变化量,例如,让立方体每个轴向随着帧的更新加上 0.02,那么可以在 renderScene 中更新旋转的值:

1
2
3
4
5
6
7
8
9
10
function renderScene() {
stats.update();

cube.rotation.x += 0.02;
cube.rotation.y += 0.02;
cube.rotation.z += 0.02;

frame = requestAnimationFrame(renderScene)
renderer.render(scene, camera);
}

现在场景里立方体就开始缓慢旋转了。

球体的弹跳

为了让球体来回运动,三角函数肯定是免不了的:

1
2
3
4
5
6
7
8
9
10
11
12
13
let step = 0;
function renderScene() {
stats.update();

// ...

step += 0.04;
sphere.position.x = 20 + 10 * (Math.cos(step));
sphere.position.y = 2 + 10 * Math.abs(Math.sin(step));

frame = requestAnimationFrame(renderScene)
renderer.render(scene, camera);
}

忽略低劣的 Gif 分辨率,看看动画就行了:

场景动画效果

本文内容参考自 《Learning Three.js》

dat.GUI

为了在浏览器上直接操作动画的速度,可以使用 Google 开源的 dat.gui.js。

应该很熟悉了:

1
2
yarn add dat.gui
yarn add @types/dat.gui --dev

使用方式都差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { GUI } from 'dat.gui';

// ...

let controls = new function() {
this.rotationSpeed = 0.02;
this.bouncingSpeed = 0.03;
}

function initGUI(controls) {
let gui = new GUI();
gui.add(controls, 'rotationSpeed', 0, 0.5);
gui.add(controls, 'bouncingSpeed', 0, 0.5);
return gui;
}

// ...

onMount(() => {
domElem.appendChild(renderer.domElement);
let gui = initGUI(controls);
domElem.appendChild(gui.domElement);

// ...

使用 dat.gui 控制变量:

1
2
3
4
5
cube.rotation.x += controls.rotationSpeed;
cube.rotation.y += controls.rotationSpeed;
cube.rotation.z += controls.rotationSpeed;

step += controls.bouncingSpeed;

同样可以考虑修改定位,让它在元素的右上角。由于 div 元素是块级元素,可以修改 display 让它变成行内元素。

1
2
3
4
.three-scene {
position: relative;
display: inline-block;
}

类似地,在 initGUI 中添加:

1
2
3
gui.domElement.style.position = "absolute";
gui.domElement.style.right = "0px";
gui.domElement.style.top = "0px";

由于帧率和分辨率的原因,下面 gif 看看就行,并不代表实际效果这么劣质

使用dat.gui控制参数

控制场景旋转

可以使用轨迹球控制场景旋转,这里用到的是 three.js 自带的 js,可以不需要另外导入新的库:

1
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js';

初始化的过程没什么意外:

1
2
3
4
5
6
7
8
9
10
11
12
function initTrackballControls(camera, renderer) {
let trackballControls = new TrackballControls(camera, renderer.domElement);
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.2;
trackballControls.panSpeed = 0.8;
trackballControls.noZoom = false;
trackballControls.noPan = false;
trackballControls.staticMoving = true;
trackballControls.dynamicDampingFactor = 0.3;
trackballControls.keys = [65, 83, 68];
return trackballControls;
}

onMount 中声明:

1
2
3
4
5
6
7
let trackballControls = initTrackballControls(camera, renderer);
let clock = new THREE.Clock();

// ...
function renderScene() {
trackballControls.update(clock.getDelta());
// ...

这样就可以用鼠标控制相机了:

轨迹球控制相机

监听视口大小改变

如果将场景铺满视口,自然会希望当用户改变窗口大小的时候,自动重新渲染场景。要实现这个操作,可以将默认的长宽设置为 window.innerWidthwindow.innerHeight。之后加入针对 window 对象的 resize 事件的监听。

在 App.svelte 中的 <script> 中加入响应函数:

1
2
3
4
5
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight)
}

添加监听器:

1
2
3
4
5
6
7
...
</script>

<svelte:window on:resize={onResize} />

<main>
...

这样就改变窗口大小就会自动设置渲染的大小和相机位置了。

小结

到了这里,three.js 的基础框架才算完成了。