【可视化学习】21-CSS渲染器与曲线运动
发表于:2023-06-10 |

前言

今天给大家带来Three.js中的CSS渲染器的使用,以及如何实现曲线运动

基础代码

先用基础代码实现一个小球

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
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

let camera, scene, renderer;

init();
animate();


function init() {
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
200
);
camera.position.set(0, 5, -10);

scene = new THREE.Scene();

const dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(0, 0, 1);
scene.add(dirLight);
const light = new THREE.AmbientLight(0xffffff, 0.5); // soft white light
scene.add(light);

// 创建小球
const sphereGeometry = new THREE.SphereBufferGeometry(1, 20, 20);
const material = new THREE.MeshStandardMaterial({
metalness: 0.7,
roughness: 0.1,
});
const sphere = new THREE.Mesh(sphereGeometry, material);
scene.add(sphere);

// 渲染器
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;
window.addEventListener("resize", onWindowResize);
}

// 监听窗口变化
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}

// 动画帧
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}

效果图

将地球月球引入

引入贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const textureLoader = new THREE.TextureLoader();
let moon;
// 添加地球
const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
const earthMaterial = new THREE.MeshPhongMaterial({
specular: 0x333333,
shininess: 5,
map: textureLoader.load("textures/planets/earth_atmos_2048.jpg"),
specularMap: textureLoader.load("textures/planets/earth_specular_2048.jpg"),
normalMap: textureLoader.load("textures/planets/earth_normal_2048.jpg"),
normalScale: new THREE.Vector2(0.85, 0.85),
});
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);

// 添加月球
const moonGeometry = new THREE.SphereGeometry(MOON_RADIUS, 16, 16);
const moonMaterial = new THREE.MeshPhongMaterial({
shininess: 5,
map: textureLoader.load("textures/planets/moon_1024.jpg"),
});
moon = new THREE.Mesh(moonGeometry, moonMaterial);
scene.add(moon);

让月球旋转

1
2
3
4
5
6
7
8
const clock = new THREE.Clock();
// 动画帧
function animate() {
const elapsed = clock.getElapsedTime();
requestAnimationFrame(animate);
moon.position.set(Math.sin(elapsed) * 5, 0, Math.cos(elapsed) * 5);
renderer.render(scene, camera);
}

效果图

添加地球label标签

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
   let labelRenderer;
// 添加提示标签
const earthDiv = document.createElement('div');
earthDiv.className = "label";
earthDiv.innerHTML = "地球";
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(0, 1, 0);
earth.add(earthLabel);
// 实例化css2d的渲染器
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(labelRenderer.domElement);
// 用来调整样式,将两个render重叠
labelRenderer.domElement.style.position = 'fixed';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
labelRenderer.domElement.style.zIndex = '10';
// 动画帧
function animate() {
const elapsed = clock.getElapsedTime();
requestAnimationFrame(animate);
moon.position.set(Math.sin(elapsed) * 5, 0, Math.cos(elapsed) * 5);
// 标签渲染器渲染
labelRenderer.render(scene, camera);
renderer.render(scene, camera);
}

效果图

让label动起来

这时我们发现,我们的control失效了,因为这时候需要用到的renderer是labelRenderer,而不是renderer,所以我们需要将control的renderer改为labelRenderer

1
2
3
4
// 控制器
const controls = new OrbitControls(camera, labelRenderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;

效果图

添加月球标签

1
2
3
4
5
6
const moonDiv = document.createElement('div');
moonDiv.className = "label";
moonDiv.innerHTML = "月球";
const moonLabel = new CSS2DObject(moonDiv);
moonLabel.position.set(0, 0.3, 0);
moon.add(moonLabel);

添加中国标签

1
2
3
4
5
6
7
// 中国
const chinaDiv = document.createElement('div');
chinaDiv.className = "label";
chinaDiv.innerHTML = "中国";
chinaLabel = new CSS2DObject(chinaDiv);
chinaLabel.position.set(-0.3, 0.5, -0.9);
earth.add(chinaLabel);

效果图

地球转动中国的标签允许隐藏

上面的效果图我们可以看到,中国的标签不应该随着地球旋转一直被看到,也就是当地球遮挡住中国的标签时,标签应该隐藏,这时我们想到了可以射线碰撞判断哪个点先接触。如果是标签先接触,说明标签应该展示,如果时地球先接触,那么就是标签该隐藏,如果射线没经过,说明标签也该展示

设置样式

1
2
3
4
5
6
7
8
9
.label1{
color: #fff;
display: none;
font-size: 1rem;
}

.label1.visible{
display: block;
}

添加碰撞逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 克隆一个位置信息,以免影响chinaLabel的原始position值
const chinaPosition = chinaLabel.position.clone();
// 计算出标签跟摄像机的距离
const labelDistance = chinaPosition.distanceTo(camera.position);
// 检测射线的碰撞
// 向量(坐标)从世界空间投影到相机的标准化设备坐标 (NDC) 空间。
chinaPosition.project(camera);
raycaster.setFromCamera(chinaPosition, camera);
const intersects = raycaster.intersectObjects(scene.children, true)
// 如果没有碰撞到任何物体,那么让标签显示
if (intersects.length == 0) {
chinaLabel.element.classList.add('visible');

} else {
// if(labelDistance)
const minDistance = intersects[0].distance;
if (minDistance < labelDistance) {
chinaLabel.element.classList.remove('visible');
} else {
chinaLabel.element.classList.add('visible');
}

}

效果图

以上部分完整代码

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import {
CSS2DRenderer,
CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer.js";
let camera, scene, renderer, labelRenderer;

const textureLoader = new THREE.TextureLoader();
const clock = new THREE.Clock();
const raycaster = new THREE.Raycaster();
init();
animate();

let moon;
let chinaLabel;


function init() {
const EARTH_RADIUS = 1;
const MOON_RADIUS = 0.27;

camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
200
);
camera.position.set(0, 5, -10);

scene = new THREE.Scene();

const dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(0, 0, 1);
scene.add(dirLight);
const light = new THREE.AmbientLight(0xffffff, 0.5); // soft white light
scene.add(light);

// 添加地球
const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
const earthMaterial = new THREE.MeshPhongMaterial({
specular: 0x333333,
shininess: 5,
map: textureLoader.load("textures/planets/earth_atmos_2048.jpg"),
specularMap: textureLoader.load("textures/planets/earth_specular_2048.jpg"),
normalMap: textureLoader.load("textures/planets/earth_normal_2048.jpg"),
normalScale: new THREE.Vector2(0.85, 0.85),
});
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);

// 添加月球
const moonGeometry = new THREE.SphereGeometry(MOON_RADIUS, 16, 16);
const moonMaterial = new THREE.MeshPhongMaterial({
shininess: 5,
map: textureLoader.load("textures/planets/moon_1024.jpg"),
});
moon = new THREE.Mesh(moonGeometry, moonMaterial);
scene.add(moon);

// 添加提示标签
const earthDiv = document.createElement('div');
earthDiv.className = "label";
earthDiv.innerHTML = "地球";
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(0, 1, 0);
earth.add(earthLabel);

// 中国
const chinaDiv = document.createElement('div');
chinaDiv.className = "label1";
chinaDiv.innerHTML = "中国";
chinaLabel = new CSS2DObject(chinaDiv);
chinaLabel.position.set(-0.3, 0.5, -0.9);
earth.add(chinaLabel);

const moonDiv = document.createElement('div');
moonDiv.className = "label";
moonDiv.innerHTML = "月球";
const moonLabel = new CSS2DObject(moonDiv);
moonLabel.position.set(0, 0.3, 0);
moon.add(moonLabel);

// 实例化css2d的渲染器
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(labelRenderer.domElement)
labelRenderer.domElement.style.position = 'fixed';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
labelRenderer.domElement.style.zIndex = '10';

// 渲染器
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 控制器
const controls = new OrbitControls(camera, labelRenderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;
window.addEventListener("resize", onWindowResize);
}

// 监听窗口变化
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}

// 动画帧
function animate() {
const elapsed = clock.getElapsedTime();
requestAnimationFrame(animate);
moon.position.set(Math.sin(elapsed) * 5, 0, Math.cos(elapsed) * 5);
const chinaPosition = chinaLabel.position.clone();
// 计算出标签跟摄像机的距离
const labelDistance = chinaPosition.distanceTo(camera.position);
// 检测射线的碰撞
// 向量(坐标)从世界空间投影到相机的标准化设备坐标 (NDC) 空间。
chinaPosition.project(camera);
raycaster.setFromCamera(chinaPosition, camera);

const intersects = raycaster.intersectObjects(scene.children, true)
// console.log(intersects)

// 如果没有碰撞到任何物体,那么让标签显示
if (intersects.length == 0) {
chinaLabel.element.classList.add('visible');

} else {
// if(labelDistance)
const minDistance = intersects[0].distance;
console.log(minDistance, labelDistance)
if (minDistance < labelDistance) {
chinaLabel.element.classList.remove('visible');
} else {
chinaLabel.element.classList.add('visible');
}

}
// 标签渲染器渲染
labelRenderer.render(scene, camera);
renderer.render(scene, camera);
}

绘制一条曲线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//根据这一系列的点创建曲线(true是闭合的曲线,默认为false)
curve = new THREE.CatmullRomCurve3(
[
new THREE.Vector3(-10, 0, 10),
new THREE.Vector3(-5, 5, 5),
new THREE.Vector3(0, 0, 5),
new THREE.Vector3(5, -5, 5),
new THREE.Vector3(10, 0, 10),
],
true
);

// 在曲线里,getPoints获取501个点,下面这个500是分割的份数
const points = curve.getPoints(500);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
const curveObject = new THREE.Line(geometry, material);
scene.add(curveObject);

效果图

让月球按照曲线运动

1
2
3
4
5
const elapsed = clock.getElapsedTime();
const time = elapsed / 10 % 1;
// 每多少秒获取点,上面是10s
const point = curve.getPoint(time);
moon.position.copy(point);

效果图

让相机朝向按照曲线运动

1
2
camera.position.copy(point);
camera.lookAt(earth.position)

效果图

曲线运动部分完整代码

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

let camera, scene, renderer, labelRenderer;

const clock = new THREE.Clock();
const textureLoader = new THREE.TextureLoader();

let moon;
let earth;
let curve;

init();
animate();

// 创建射线

function init() {
const EARTH_RADIUS = 1;
const MOON_RADIUS = 0.27;

camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
200
);
camera.position.set(0, 5, -10);

scene = new THREE.Scene();

const dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(0, 0, -1);
scene.add(dirLight);
const light = new THREE.AmbientLight(0xffffff, 0.5); // soft white light
scene.add(light);

// const axesHelper = new THREE.AxesHelper( 5 );
// scene.add( axesHelper );

//

const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
const earthMaterial = new THREE.MeshPhongMaterial({
specular: 0x333333,
shininess: 5,
map: textureLoader.load("textures/planets/earth_atmos_2048.jpg"),
specularMap: textureLoader.load("textures/planets/earth_specular_2048.jpg"),
normalMap: textureLoader.load("textures/planets/earth_normal_2048.jpg"),
normalScale: new THREE.Vector2(0.85, 0.85),
});

earth = new THREE.Mesh(earthGeometry, earthMaterial);
// earth.rotation.y = Math.PI;
scene.add(earth);

const moonGeometry = new THREE.SphereGeometry(MOON_RADIUS, 16, 16);
const moonMaterial = new THREE.MeshPhongMaterial({
shininess: 5,
map: textureLoader.load("textures/planets/moon_1024.jpg"),
});
moon = new THREE.Mesh(moonGeometry, moonMaterial);
scene.add(moon);

//根据这一系列的点创建曲线
curve = new THREE.CatmullRomCurve3(
[
new THREE.Vector3(-10, 0, 10),
new THREE.Vector3(-5, 5, 5),
new THREE.Vector3(0, 0, 5),
new THREE.Vector3(5, -5, 5),
new THREE.Vector3(10, 0, 10),
],
true
);

// 在曲线里,getPoints获取51个点
const points = curve.getPoints(500);
console.log(points);
const geometry = new THREE.BufferGeometry().setFromPoints(points);

const material = new THREE.LineBasicMaterial({ color: 0xff0000 });

// Create the final object to add to the scene
const curveObject = new THREE.Line(geometry, material);
scene.add(curveObject);

renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;

//

window.addEventListener("resize", onWindowResize);
}

function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;

camera.updateProjectionMatrix();

renderer.setSize(window.innerWidth, window.innerHeight);
}


function animate() {
requestAnimationFrame(animate);

const elapsed = clock.getElapsedTime();
const time = elapsed / 10 % 1;
const point = curve.getPoint(time);
// console.log(point)
moon.position.copy(point);
// moon.position.set(Math.sin(elapsed) * 5, 0, Math.cos(elapsed) * 5);
camera.position.copy(point);
camera.lookAt(earth.position)
renderer.render(scene, camera);
}

上一篇:
【可视化学习】22-Three.js物体形变
下一篇:
获取文本所占的长度