Appearance
本文主要是在官网的基础案例上进行了一些修改,主要是为了学习threejs的css2d标准渲染。
思路
首先我们需要构建一个场景,弄一个月球和地球
js
const EARTH_RADIUS = 3;
const MOON_RADIUS = 0.57;
const earthMaterial = new THREE.MeshPhongMaterial({
specular: 0x333333,
shininess: 5,
map: textureLoader.load('/ziyuan/planets/earth_atmos_2048.jpg'),
specularMap: textureLoader.load('/ziyuan/planets/earth_specular_2048.jpg'),
normalMap: textureLoader.load('/ziyuan/planets/earth_normal_2048.jpg'),
normalScale: new THREE.Vector2(0.85, 0.85)
});
earthMaterial.map.colorSpace = THREE.SRGBColorSpace;
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('/ziyuan/planets/moon_1024.jpg')
});
moonMaterial.map.colorSpace = THREE.SRGBColorSpace;
moon = new THREE.Mesh(moonGeometry, moonMaterial);
scene.add(moon);接着利用CSS2DRenderer渲染器,将月球和地球的名字渲染到场景中。
js
const earthDiv = document.createElement('div');
earthDiv.className = 'label';
earthDiv.textContent = '我是地球';
earthDiv.style.backgroundColor = 'transparent';
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(0, 1.5 * EARTH_RADIUS, 0);
earthLabel.center.set(0.5, 0);
earth.add(earthLabel);
const moonDiv = document.createElement('div');
moonDiv.className = 'label';
moonDiv.textContent = '我是月球';
moonDiv.style.backgroundColor = 'transparent';
const moonLabel = new CSS2DObject(moonDiv);
moonLabel.position.set(0, 1.8 * MOON_RADIUS, 0);
moonLabel.center.set(0.5, 0);
// 创建CSS2DRenderer渲染器对象
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
document.getElementById('canvas').appendChild(labelRenderer.domElement);
// render
labelRenderer.render(scene, camera);到了这一步,月亮和地球的名字就已经渲染到场景中了。
接下来如何让月球转起来呢 render 阶段不停的改变位置就能转起来
js
const elapsed = clock.getElapsedTime();
moon.position.set(Math.sin(elapsed) * 8, 0, Math.cos(elapsed) * 8);此时发现月球是近大远小的感觉,而我们的文字确实一直不变,效果不好,根据距离我们不停的的改变字体大小来解决这个问题
js
const distance = moon.position.distanceTo(camera.position);
let scale = 1 / distance; // 根据距离调整大小
const fontSize = 12 * scale * 30; // 基础字体大小乘以缩放比例
moonLabel.element.style.fontSize = `${fontSize}px`;OK,到这一步感觉就上来了,但是当月球转到地球后面,月球看不到了,文字还能看到是为啥
因为文字和月球是两个渲染器,而且文字的渲染器高于地球,因此不会被地球挡住,所以,此时我们可以用之前学的Raycaster射线思想来解决这个问题,判断交点来控制文字的显示隐藏即可
js
const origin = camera.position.clone();
const direction = moon.position.clone().sub(camera.position).normalize();
raycaster.set(origin, direction);
// 检测射线与地球和月球的交点
const intersects = raycaster.intersectObjects([earth, moon], true);
// 判断月球是否被地球遮挡
let isMoonHidden = false;
let earthIntersected = false;
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object === earth) {
earthIntersected = true;
break;
}
}
if (earthIntersected) {
// 如果射线与地球相交,检查月球是否在地球后面
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object === moon) {
if (intersects[i].distance > intersects[0].distance) {
isMoonHidden = true;
}
break;
}
}
}
moonLabel.element.style.opacity = isMoonHidden ? 0 : 1;效果
完整代码
html
<template>
<div id="canvas"></div>
</template>
<script setup>
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { onMounted } from 'vue';
onMounted(() => {
let gui;
let camera, scene, renderer, labelRenderer;
const clock = new THREE.Clock();
const textureLoader = new THREE.TextureLoader();
let moon;
const EARTH_RADIUS = 3;
const MOON_RADIUS = 0.57;
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(0, 0, 20);
camera.layers.enableAll();
scene = new THREE.Scene();
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
dirLight.position.set(0, 0, 1);
dirLight.layers.enableAll();
scene.add(dirLight);
const axesHelper = new THREE.AxesHelper(10);
axesHelper.layers.enableAll();
scene.add(axesHelper);
const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
const earthMaterial = new THREE.MeshPhongMaterial({
specular: 0x333333,
shininess: 5,
map: textureLoader.load('/ziyuan/planets/earth_atmos_2048.jpg'),
specularMap: textureLoader.load('/ziyuan/planets/earth_specular_2048.jpg'),
normalMap: textureLoader.load('/ziyuan/planets/earth_normal_2048.jpg'),
normalScale: new THREE.Vector2(0.85, 0.85)
});
earthMaterial.map.colorSpace = THREE.SRGBColorSpace;
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);
const earthDiv = document.createElement('div');
earthDiv.className = 'label';
earthDiv.textContent = '我是地球';
earthDiv.style.backgroundColor = 'transparent';
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(0, 1.5 * EARTH_RADIUS, 0);
earthLabel.center.set(0.5, 0);
earth.add(earthLabel);
// earthLabel.layers.set(0);
const moonGeometry = new THREE.SphereGeometry(MOON_RADIUS, 16, 16);
const moonMaterial = new THREE.MeshPhongMaterial({
shininess: 5,
map: textureLoader.load('/ziyuan/planets/moon_1024.jpg')
});
moonMaterial.map.colorSpace = THREE.SRGBColorSpace;
moon = new THREE.Mesh(moonGeometry, moonMaterial);
scene.add(moon);
const moonDiv = document.createElement('div');
moonDiv.className = 'label';
moonDiv.textContent = '我是月球';
moonDiv.style.backgroundColor = 'transparent';
const moonLabel = new CSS2DObject(moonDiv);
moonLabel.position.set(0, 1.8 * MOON_RADIUS, 0);
moonLabel.center.set(0.5, 0);
// moonLabel.layers.set(moon.layers.mask);
moon.add(moonLabel);
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('canvas').appendChild(renderer.domElement);
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
document.getElementById('canvas').appendChild(labelRenderer.domElement);
const controls = new OrbitControls(camera, labelRenderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;
const raycaster = new THREE.Raycaster();
function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
moon.position.set(Math.sin(elapsed) * 8, 0, Math.cos(elapsed) * 8);
// moonLabel.position.copy(moon.position).add(new THREE.Vector3(0, 1.8 * MOON_RADIUS, 0));
const origin = camera.position.clone();
const direction = moon.position.clone().sub(camera.position).normalize();
raycaster.set(origin, direction);
// 检测射线与地球和月球的交点
const intersects = raycaster.intersectObjects([earth, moon], true);
// 判断月球是否被地球遮挡
let isMoonHidden = false;
let earthIntersected = false;
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object === earth) {
earthIntersected = true;
break;
}
}
if (earthIntersected) {
// 如果射线与地球相交,检查月球是否在地球后面
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object === moon) {
if (intersects[i].distance > intersects[0].distance) {
isMoonHidden = true;
}
break;
}
}
}
moonLabel.element.style.opacity = isMoonHidden ? 0 : 1;
console.log('intersects', intersects);
const distance = moon.position.distanceTo(camera.position);
let scale = 1 / distance; // 根据距离调整大小
const fontSize = 12 * scale * 30; // 基础字体大小乘以缩放比例
moonLabel.element.style.fontSize = `${fontSize}px`;
// moonLabel.scale.set(scale, scale, scale);
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
}
animate();
});
</script>
<style >
.label {
color: #fff;
font-family: sans-serif;
padding: 2px;
background: rgba(0, 0, 0, 0.6);
}
</style>