Skip to content

css2d标注渲染

🕒 Published at:

本文主要是在官网的基础案例上进行了一些修改,主要是为了学习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>