Skip to content

48. CSS2DRenderer 实现标注:给 3D 物体加标签

Published:

很多时候,我们希望给 3D 场景中的物体加一些标注信息:

比如点击某个物体的时候,弹出一个介绍框:

image.png

这种标注怎么做呢?

Three.js 提供了 CSS2DRenderer 来做这件事情。

它是用 html 渲染的,覆盖在 three.js 的那层 canvas 之上。

我们试一下:

npx create-vite css2d-annotation

image.png

进入项目,安装依赖:

npm install
npm install --save three
npm install --save-dev @types/three

改下 src/main.js

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';

const scene = new THREE.Scene();

scene.add(mesh);

const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(500, 400, 300);
scene.add(directionLight);

const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);

const width = window.innerWidth;
const height = window.innerHeight;

const helper = new THREE.AxesHelper(500);
scene.add(helper);

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(500, 600, 800);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Light、Camera、Renderer。

改下 style.css

body {
  margin: 0;
}

然后创建 mesh.js

import * as THREE from 'three';

const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
const planeMaterial = new THREE.MeshLambertMaterial({
    color: new THREE.Color('skyblue')
});

const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotateX(- Math.PI / 2);
plane.position.y = -50;

const boxGeometry = new THREE.BoxGeometry(100, 100, 100);
const boxMaterial = new THREE.MeshLambertMaterial({
    color: new THREE.Color('orange')
});
const box = new THREE.Mesh(boxGeometry, boxMaterial);

const box2 = box.clone();
box2.position.x = 200;

const mesh = new THREE.Group();
mesh.add(plane);
mesh.add(box);
mesh.add(box2);

export default mesh;

就是一个平面,两个立方体。

跑一下:

npm run dev

image.png

看下效果:

image.png

然后我们想在点击的时候加一个标注信息。

这时候就可以用 CSS2DRenderer

image.png

image.png

import { CSS2DObject } from 'three/examples/jsm/Addons.js';
const ele = document.createElement('div');
ele.innerHTML = '<p style="background:#fff;padding: 10px;">这是 box1</p>';
const obj = new CSS2DObject(ele);
obj.position.y = 100;
box.add(obj);

const ele2 = document.createElement('div');
ele2.innerHTML = '<p style="background:#fff;padding: 10px;">这是 box2</p>';
const obj2 = new CSS2DObject(ele2);
obj2.position.y = 100;
box2.add(obj2);

分别在 box 和 box2 下面添加一个 CSS2DObject,它的参数是一个 dom 元素。

然后还要在 main.js 里加一个新的渲染器:

image.png

import { CSS2DRenderer } from 'three/examples/jsm/Addons.js';
const css2Renderer = new CSS2DRenderer();
css2Renderer.setSize(width, height);

const div = document.createElement('div');
div.style.position = 'relative';
div.appendChild(css2Renderer.domElement);
css2Renderer.domElement.style.position = 'absolute';
css2Renderer.domElement.style.left = '0px';
css2Renderer.domElement.style.top = '0px';
css2Renderer.domElement.style.pointerEvents = 'none';

div.appendChild(renderer.domElement);
document.body.appendChild(div);

function render() {
    css2Renderer.render(scene, camera);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

// document.body.append(renderer.domElement);

创建 CSS2DRenderer,它也会返回一个 domElement

我们创建一个 div,把两个 domElement 放进去,并且让 css2dRenderer.domElement 绝对定位并且不响应鼠标事件。

在 render 循环里调用 css2dRenderer.render 来一帧帧渲染。

看下效果:

2025-04-07 13.38.23.gif

这样,我们就给 3D 场景中的物体加上了标注信息。

那它的原理是什么呢?

我们打开 devtools 看一下:

image.png

可以看到,最外层 div 下一个是 WebGLRenderer 渲染的 canvas,另一个就是 CSS2DRenderer 渲染的 div 了。

这个 div 下有两个 div 放着那俩标签。

当我们移动物体位置或者相机位置的时候,它们的位置也会跟着变:

2025-04-07 13.47.44.gif

这就是它的原理。

那它是怎么从 3D 世界的坐标转换为屏幕坐标的呢?

还记得我们之前讲的射线和点击么?

它是从屏幕坐标转化为 3D 场景中的坐标:

image.png

2025-04-01 17.53.08.gif

那反过来从 3D 场景中的坐标算屏幕坐标自然也可以做到。

此外,之前一直没讲窗口 resize 时的处理:

2025-04-07 13.51.56.gif

这里顺便讲一下。

image.png

window.onresize = function () {
  const width = window.innerWidth;
  const height = window.innerHeight;

  renderer.setSize(width,height);
  css2Renderer.setSize(width,height);

  camera.aspect = width / height;
  camera.updateProjectionMatrix();
};

只要在窗口 resize 的时候重新计算下宽高比,调整下 renderer 的 size,更新下相机的 aspect 参数就好了,调完相机参数还要 updateProjectionMatrix 更新相机投影矩阵才能生效。

2025-04-07 13.56.04.gif

这样就好了。

这个标注信息可以点击的时候再显示:

image.png

obj2.name = 'tag';
obj2.visible = false;

加个 name,然后设置 visible 为 false。

点击的时候再显示:

image.png

renderer.domElement.addEventListener('click', (e) => {
  const y = -((e.offsetY / height) * 2 - 1);
  const x = (e.offsetX / width) * 2 - 1;

  const rayCaster = new THREE.Raycaster();
  rayCaster.setFromCamera(new THREE.Vector2(x, y), camera);

  const intersections = rayCaster.intersectObjects(mesh.children);

  if(intersections.length) {
    const obj = intersections[0].object;
    const tag = obj.getObjectByName('tag');
    if(tag) {
        tag.visible = !tag.visible;
    }
  }
});

点击的时候,拿到射线射中的物体,根据 name 查找标签,设置 visible 就好了。

2025-04-07 14.45.06.gif

案例代码上传了小册仓库

总结

这节我们学了用 CSS2DRenderer 实现信息标注。

它是通过在 canvas 元素上加一层 div,根据 3D 物体的位置来计算出屏幕坐标的位置,调整标签位置,来实现在 3D 物体上加标注的功能。

咋要标注的物体上加一个 CSS2DObject,传入 dom 元素,这样就会在那里展示一个标注。

可以最开始设置标注的 visible 为 false,然后点击的时候再设置为 true,这样就是点击的时候显示标注的效果。

3D 场景的标注在开发的过程中用的很多,后面会经常用到。

评论