likes
comments
collection
share

Three.js- CSS2DRenderer 在3D场景中添加标签

作者站长头像
站长
· 阅读数 5

一、 CSS2DRenderer

css2DRenderer是一种特殊的渲染器,用于将HTML嵌到3D场景中,它可以让我们在3D场景中添加并显示 2D 的 HTML 元素。这对于创建用户界面和展示信息非常有用。比如在VR看房中,每个房间会有一个标签,如厨房、客厅、卧室等,点击这些标签会展示不同的房间。这些标签是通过HTML创建的,并通过与这些HTML标签交互来控制3D对象的显示与操作。

接下来,让我们来看下如何在3D场景中添加HTML。

以官网的这个例子为例:

创建两个球体,分别为地球和月球,并给球体加上贴图。然后让月球围绕地球周期性运动,代码如下:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
let camera, scene, renderer;

const clock = new THREE.Clock();
// 纹理加载器
const textureLoader = new THREE.TextureLoader();
let moon;
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);
  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);

  renderer = new THREE.WebGLRenderer();
  // 设置像素比(css像素和物理像素的比例)
  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);
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
}


function animate() {
  requestAnimationFrame(animate);
  const elapsed = clock.getElapsedTime();
  // 月亮周期性运动
  moon.position.set(Math.sin(elapsed) * 5, 0, Math.cos(elapsed) * 5);
  renderer.render(scene, camera);
}

效果:

Three.js- CSS2DRenderer 在3D场景中添加标签

接下来,给这两个球体加上标签:

导入CSS2DRendererCSS2DObject

CSS2DObject用于将HTML元素包装成可以在3D场景中渲染的对象,它将HTML元素与three.js中的场景连接,使得该元素能够根据物体位置和场景的相机位置自动定位和渲染。

import {
  CSS2DRenderer,
  CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer.js";

创建标签、实例化CSS2DRenderer

// 创建标签 在init函数中
const earthDiv = document.createElement('div');
earthDiv.className = 'label';
earthDiv.innerHTML = '地球'
// 创建css2d对象。用CSS2DObject包装HTML
const earthLabel = new CSS2DObject(earthDiv);
// 设置2d对象的位置
earthLabel.position.set(0,1,0);
// 将2d对象添加到地球上
earth.add(earthLabel)

// 实例化CSS2DRenderer 。labelRenderer在函数外面定义
labelRenderer = new CSS2DRenderer()
// 设置渲染器宽高
labelRenderer.setSize(window.innerWidth,innerHeight);
//将css2d节点渲染到页面中
document.body.appendChild(labelRenderer.domElement);

//设置2d渲染器布局
labelRenderer.domElement.style.position = 'fixed';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
labelRenderer.domElement.style.zIndex = '0';

function animate() {
    // 更新并渲染CSS2DRenderer
    labelRenderer.render(scene,camera);
}
.label{
  color: #fff;
  font-size: 1rem;
}

效果:标签被添加到了地球上方。

Three.js- CSS2DRenderer 在3D场景中添加标签

但是有一个问题,轨道控制器不生效了,也就是场景不能跟随相机旋转、缩放、平移了。原因是CSS2DRenderer渲染器的domElement的层级在WebGLRendererdomElement的上面,阻止了鼠标事件的触发。

可以这样修改:OrbitControls使用CSS2DRendererdomElement

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

或者:也可以这样修改:设置两个HTML标签,将这两个渲染器分别添加到不同的标签下,然后禁用CSS2D渲染器所在标签的鼠标事件。

<div id="3d-container">
    <div id="webgl-output"></div>
    <div id="css2d-output"></div>
</div>
#3d-container {
    position: relative;
}

#webgl-output,
#css2d-output {
    position: absolute;
    top: 0;
    left: 0;
}

#webgl-output {
    z-index: 1;
}
#css2d-output {
    z-index: 2;
    pointer-events: none;  // 禁用鼠标事件
}

var webglRenderer = new THREE.WebGLRenderer();
document.getElementById('webgl-output').appendChild(webglRenderer.domElement);

var css2dRenderer = new THREE.CSS2DRenderer();
document.getElementById('css2d-output').appendChild(css2dRenderer.domElement);

var controls = new THREE.OrbitControls(camera, webglRenderer.domElement);

这样就可以对物体进行旋转、缩放、移动操作了。

那么接下来,如法炮制,添加月球和中国标签。

月球

//添加月球标签
const moonDiv = document.createElement('div');
moonDiv.className = "label";
moonDiv.innerHTML = "月球";
//创建css2d对象
const moonLabel = new CSS2DObject(moonDiv);
//设置2d对象的位置
moonLabel.position.set(0,0.5,0);
//将2d对象添加到月球上
moon.add(moonLabel);

中国

//添加中国标签
const chinaDiv = document.createElement('div');
chinaDiv.className = "label1";
chinaDiv.innerHTML = "中国";
//创建css2d对象
const chinaLabel = new CSS2DObject(chinaDiv);
//设置2d对象的位置
chinaLabel.position.set(-0.3,0.5,-0.9);
//将2d对象添加到地球上
earth.add(chinaLabel);

效果:

Three.js- CSS2DRenderer 在3D场景中添加标签

当转动地球的时候,'中国'这个标签应该跟着地图的位置显示隐藏。如果地球转动到后面,标签应该被隐藏,转动到前面,标签应该被显示。

我们用RayCaster(光线投射)对象进行射线与物体的碰撞检测。射线是从摄像机指向标签位置。如果标签被遮挡,射线和球体会有交点。如果没有交点,代表标签位于相机前面,则应该显示标签。如果有交点,要判断交点(球)到摄像机的距离和标签到摄像机的距离,如果交点到摄像机距离小于标签到摄像机距离,代表标签被地球挡住了,此时应该隐藏标签,反之,显示标签。

几个点: 1、标签位置是固定的,改变的相机的视角 2、射线是根据标签位置计算的,射线始终从摄像机指向标签位置,方向也是固定的 3、球体是通过轨道控制器进行旋转和移动的,它的相对方向和位置是变化的

代码如下:

.label1{
  color: #fff;
  display: none;
  font-size: 1rem;
}
.label1.visible{
  display: block;
}
// 标签的位置
const chinaPosition = chinaLabel.position.clone();
// 计算相机和标签之间的距离 distanceTo:计算两个点之间的直线距离
const labelDistance = chinaPosition.distanceTo(camera.position)
// 将标签的位置向量从世界空间投影到摄像机的标准化设备坐标空间(三维坐标到二维屏幕坐标)
chinaPosition.project(camera);
// 根据chinaPosition的屏幕坐标,设置射线的起点和方向
raycaster.setFromCamera(chinaPosition,camera);
// 检测射线是否与场景中的其他对象相交
const intersects = raycaster.intersectObjects(scene.children,true);

// 如果没有交点,也就是标签位于球体上方,则显示标签
if(!intersects.length) {
    chinaLabel.element.classList.add('visible');
} else {
    // 如果有交点,需要进一步判断,球体在标签前面还是在标签后面。在标签前面代表挡住了标签,这时隐藏标签。否则显示标签;
    // 获取最近相交点(球体)到相机的距离
    const minDistance = intersects[0].distance;
    if(minDistance < labelDistance) { // 球体比标签离相机更近,球体位于标签前面挡住了标签,所以要隐藏标签
        chinaLabel.element.classList.remove('visible')
    } else { // 球体位于标签后面,不遮挡标签。显示标签
        chinaLabel.element.classList.add('visible');
    }
}

最终效果:

Three.js- CSS2DRenderer 在3D场景中添加标签

二、曲线

1、Curve

Curve是一个抽象基类。通常不会直接用,而是使用其子类来创建曲线。例如,LineCurve3用于创建直线,CatmullRomCurve3用于创建Catmull-Rom样条曲线(平滑且连续的曲线),QuadraticBezierCurve3用于创建二次贝塞尔曲线,等等。

例如我们使用CatmullRomCurve3创建一条曲线,让月亮绕着该曲线运动。

let curve;
// CatmullRomCurve3:使用Catmull-Rom算法, 从一系列的点创建一条平滑的三维样条曲线。第一个参数是点数组,第二个参数代表曲线是否闭合
curve = new THREE.CatmullRomCurve3( [
   new THREE.Vector3( -10, 0, 10 ),
   new THREE.Vector3( -5, 5, 5 ),
   new THREE.Vector3( 0, 0, 0 ),
   new THREE.Vector3( 5, -5, 5 ),
   new THREE.Vector3( 10, 0, 10 )
],true);
// 将曲线划分的段数。划分50段,也就是有51个点
const points = curve.getPoints( 50 );
 // 创建一个BufferGeometry对象,将points设置为其顶点
const geometry = new THREE.BufferGeometry().setFromPoints( points );
// 基础线条材质
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
// 创建网格
const curveObject = new THREE.Line( geometry, material );
scene.add(curveObject);

在渲染函数中,获取时间并标准化到[0,1]之间。通过Curve的getPoint方法获取给定时间曲线上点的位置,将这个位置赋值给月亮,实现月亮绕曲线运动。

function animate() {  
 // 获取从开始到当前的时间
  const elapsed = clock.getElapsedTime();
  // 将时间标准化为0-1之间
  const time = elapsed/10%1;
  // 获取给定时间的曲线上的位置。getPoint方法参数必须在[0,1]之间
  const point = curve.getPoint(time);
  // 将点的坐标赋值给月球
  moon.position.copy(point);
  
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

效果:

Three.js- CSS2DRenderer 在3D场景中添加标签

2、CurvePath

CurvePath 是一个用于创建和操作由多个曲线组成的路径的类。简单来说就是连接多条曲线,形成一个连续曲线路径。

依然以上面的例子为例:创建多条曲线,添加到curvePath中。

const curve1 = new THREE.LineCurve3(
    new THREE.Vector3(-10, 0, 0),
    new THREE.Vector3(0, 10, 0)
);
const curve2 = new THREE.QuadraticBezierCurve3(
    new THREE.Vector3(0, 10, 0),
    new THREE.Vector3(10, 15, 0),
    new THREE.Vector3(20, 0, 0)
);

curve = new THREE.CurvePath();
curve.add(curve1);
curve.add(curve2);

效果:曲线1是一条直线,曲线2是三维二次贝塞尔曲线。

Three.js- CSS2DRenderer 在3D场景中添加标签

想要路径闭合,可以调用curvePathclosePath方法。这个方法会添加一条lineCurve用于闭合路径。

curve.closePath()

或者,也可以自己获取curve1的起点和 curve2的终点,然后创建一条LineCurve3连接起点和终点。代码如下

const startPoint = curve1.getPoint(0);
const endPoint = curve2.getPoint(1);
const lineCurve = new THREE.LineCurve3(endPoint, startPoint);
curve.add(lineCurve);

效果:

Three.js- CSS2DRenderer 在3D场景中添加标签

三、CSS3DRenderer

CSS3DRenderer用于将3D对象渲染成DOM元素,以便使用css进行样式设置和动画。 其原理是为CSS3DObject对象创建对应的div元素,并将三维空间内的位置、缩放、旋转信息转化为CSS3的transform属性,应用到dom上。

import { CSS3DRenderer,CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer';
// 创建CSS3D渲染器
const renderer = new CSS3DRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建CSS3D对象
const element = document.createElement('div');
element.style.width = '1px';
element.style.height = '1px';
element.style.background = 'green';
// 通过CSS3DObject将普通的HTML元素包装成CSS3D对象
const object = new CSS3DObject(element);
scene.add(object);
// 创建动画
function animate() {
    requestAnimationFrame(animate);

    // 旋转正方形
    object.rotation.x += 0.01;
    object.rotation.y += 0.01;

    renderer.render(scene, camera);
}

以上代码创建了CSS3D渲染器,然后创建了一个html元素,并使用CSS3DObject将其包装为CSS3D对象,最后在渲染函数中给这个对象添加旋转效果。

这段代码最终实现了一个旋转的绿色方块的动画效果。

Three.js- CSS2DRenderer 在3D场景中添加标签

官网例子

四、结语

遇事先站在别人的角度考虑,会消除很多的情绪与误解,情绪会让人产生偏见,带有偏见的看待人和事。看到的总是不好的一面。