Threejs中三维物体和HTML的爱恨情仇:CSS2DRenderer

简介

本教程是一个系列,如果觉得文章对你有用,欢迎订阅该系列!

上期文章:[threejs做特效:实现物体的发光效果-EffectComposer详解!]

感谢各位彦祖点赞、收藏

在threejs的开发中,我们经常会遇到三维物体附近需要渲染html标签的情况,比如下图:

Threejs中三维物体和HTML的爱恨情仇:CSS2DRenderer

其实,实现这样一个需求非常简单!我们只需要了解CSS2DObjectCSS2DRenderer两个概念即可。接下来,我们将借助CSS2DRenderer实现下面的效果:

Threejs中三维物体和HTML的爱恨情仇:CSS2DRenderer

核心API

要想实现三维物体与HTML的结合,我们必须熟悉两个概念:CSS2DRendererCSS2DObject。它们之间的关系也很纯粹:

CSS2DObject 用于表示需要在三维场景中渲染的 DOM 元素,而 CSS2DRenderer 则负责将这些元素正确地渲染到场景中。

CSS2DObject

CSS2DObject 是 Three.js 中的一个对象类型,它代表一个包含了 DOM 元素的容器,可以在 Three.js 场景中渲染。其作用是将二维的 DOM 元素嵌入到三维场景中,使其能够随着场景的交互而动态显示。

主要属性和方法:

  • position:设置对象在三维场景中的位置。
  • center:设置对象的中心点。
  • layers:设置对象的图层。
  • updateMatrixWorld():更新对象的世界矩阵,以便正确渲染到场景中。

CSS2DRenderer

CSS2DRenderer 是 Three.js 中的渲染器,专门用于渲染 CSS2DObject 对象。它的作用是将二维 DOM 元素正确地渲染到场景中,并且与 Three.js 的其他渲染器(如 WebGLRenderer)兼容,使得能够同时渲染二维和三维内容。

主要方法:

  • setSize(width, height):设置渲染器的大小,通常与窗口大小一致。
  • render(scene, camera):将指定的 Three.js 场景和相机渲染到 HTML 文档中的 DOM 元素上。

技术方案

原生html框架搭建

借助threejs实现html与三维物体的渲染,首先我们使用html搭建一个简单的开发框架

参考官方起步文档:three.js中文网

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Threejs中三维物体和HTML</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
  <link type="text/css" rel="stylesheet" href="./main.css" />
</head>

<body>
  <div id="container"></div>
  <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/three@0.163.0/build/three.module.js",
          "three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
        }
      }
    </script>

  <script type="module">
    import * as THREE from "three";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";

  </script>
</body>

</html>

上述代码中,我们采用type="importmap"的方式引入了threejs开发 的一些核心依赖,”three”是开发的最基本依赖;在Three.js中,"addons" 通常指的是一些附加功能或扩展模块,它们提供了额外的功能,可以用于增强或扩展Three.js的基本功能。

type="module"中,我们引入了OrbitControls轨道控制器

实现地球的加载

在threejs中,如果你掌握基础,加载一个地球是非常容易得

<script type="module">
  import * as THREE from 'three';
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

  let camera, scene, renderer;

  init();

  function init() {
      // 设置地球半径大小
      const EARTH_RADIUS = 2;
      // 定义相机和场景
      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
      camera.position.set(10, 5, 20);
      scene = new THREE.Scene();
      // 创建地球对象,并设置材质
      const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
      const textureLoader = new THREE.TextureLoader();
      const earthMaterial = new THREE.MeshPhongMaterial({
          specular: 0x333333,
          shininess: 5,
          map: textureLoader.load('./textures/earth_atmos_2048.jpg'),
          specularMap: textureLoader.load('./textures/earth_specular_2048.jpg'),
          normalMap: textureLoader.load('./textures/earth_normal_2048.jpg'),
          normalScale: new THREE.Vector2(0.85, 0.85)
      });
      // 设置地球材质的贴图颜色空间为SRGB色彩空间
      earthMaterial.map.colorSpace = THREE.SRGBColorSpace;
      // 创建地球mesh对象
      const earth = new THREE.Mesh(earthGeometry, earthMaterial);
      scene.add(earth);
      // 创建渲染器
      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);
      animate();
  }

  function onWindowResize() {
      // 更新相机的纵横比和投影矩阵
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      // 更新渲染器的大小
      renderer.setSize(window.innerWidth, window.innerHeight);
  }

  function animate() {
      //请求下一帧动画
      requestAnimationFrame(animate)
      //渲染场景
      renderer.render(scene, camera);
  }

这段代码创建了一个简单的Three.js场景,展示了一个带有纹理的地球模型,并实现基本的交互和窗口自适应功能。我们看看代码实现的效果:Threejs中三维物体和HTML的爱恨情仇:CSS2DRenderer

现在,我们借助CSS2DRender对这个地球场景增加一个html的简介

给场景添加HTML介绍

let camera, scene, renderer, labelRenderer;
function init() {
// ....
// 创建地球mesh对象
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);
// -----------------------------------创建地球的标题html
const earthDiv = document.createElement('div');
// 设置div类名
earthDiv.className = 'label';
// 设置div内的html
earthDiv.innerHTML = `<div>地球<span style='color:red'>简介</span></div>`;
// 使用CSS2DObject将<div>元素转换为可在Three.js场景中渲染的对象
const earthLabel = new CSS2DObject(earthDiv); 
// 设置地球标题对象在场景中的位置,X轴偏移量为地球半径的1.5倍
earthLabel.position.set(1.5 * EARTH_RADIUS, 0, 0); 
earthLabel.center.set(0, 1); // 设置地球标题对象的中心点为顶部中心
earth.add(earthLabel); // 将地球标题对象添加到地球模型上
//-------------------------------------创建地球的简介html
const earthMassDiv = document.createElement('div');
earthMassDiv.className = 'content';
earthMassDiv.innerHTML = `<div>重所周知,地球是地球!望周知!</div>`;
const earthMassLabel = new CSS2DObject(earthMassDiv);
earthMassLabel.position.set(1.5 * EARTH_RADIUS, 0, 0);
earthMassLabel.center.set(0, 0);
earth.add(earthMassLabel);
//创建场景渲染器
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建用于渲染标签的渲染器
labelRenderer = new CSS2DRenderer(); // 创建一个CSS2DRenderer渲染器
// 设置渲染器的大小为窗口大小
labelRenderer.setSize(window.innerWidth, window.innerHeight); 
// 设置渲染器的DOM元素的定位方式为绝对定位
labelRenderer.domElement.style.position = 'absolute'; 
// 设置渲染器的DOM元素的顶部偏移量为0像素
labelRenderer.domElement.style.top = '0px'; 
// 将渲染器的DOM元素添加到文档中
document.body.appendChild(labelRenderer.domElement); 
const controls = new OrbitControls(camera, labelRenderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;
window.addEventListener('resize', onWindowResize);
animate();
}
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)
// 使用渲染器渲染场景
renderer.render(scene, camera);
// 使用标签渲染器渲染场景中的标签
labelRenderer.render(scene, camera);
}

上述代码在 Three.js 场景中添加地球模型的标签,并创建了用于渲染这些标签的CSS2DRenderer。注意,我们给标签分别设置了两个label和content两个类名,因此我们还有加入相应的样式

<head>
// .....
<style>
.label {
color: #FFF;
font-family: sans-serif;
padding: 2px;
background: transparent;
}
.content {
background: red;
}
</style>
</head>

现在,我们就实现了如图的效果

Threejs中三维物体和HTML的爱恨情仇:CSS2DRenderer

如何隐藏与显示文字

要想实现文字的显示与隐藏功能,我们必须了解一个概念:Layers

Layers

我们先看官网的释义

  • Layers 对象为 Object3D 分配 1个到 32 个图层。32个图层从 0 到 31 编号标记。 在内部实现上,每个图层对象被存储为一个 bit mask, 默认的,所有 Object3D 对象都存储在第 0 个图层上
  • 图层对象可以用于控制对象的显示。当 camera 的内容被渲染时与其共享图层相同的物体会被显示。每个对象都需要与一个 camera 共享图层。
  • 每个继承自 Object3D 的对象都有一个 Object3D.layers 对象。

简单来说,在threejs中,所有的物体都位于图层中(默认都是0),我们通过控制物体的图层,就可以方便的实现物体的显示与隐藏。

我们看一个简单示例:

// 创建一个对象
const cube = new THREE.Mesh(geometry, material);
// 将对象添加到图层 1 和 2 上
cube.layers.enable(1);
cube.layers.enable(2);
// 设置渲染器的 layerMask,只渲染图层 1 和 2
renderer.layerMask = 1 | 2;
// 渲染场景
renderer.render(scene, camera);

在这个示例中,cube 对象被添加到了图层 1 和 2 上,并且设置了渲染器的 layerMask,指定了只渲染图层 1 和 2。因此,在渲染时,只有添加到图层 1 和 2 的对象会被渲染。

渲染器的 layerMask 属性: 通过设置渲染器的 layerMask 属性,可以指定渲染器只渲染特定的图层。

我们再看一些layers其他几个比较重要的属性与方法

方法名 类型定义 释义
set ( layer : Integer ) : undefined 删除图层对象已有的所有对应关系,增加与参数指定的图层的对应关系。
toggle ( layer : Integer ) : undefined 根据参数切换对象所属图层。
enableAll () : undefined 为所有层添加成员。(始终显示该对象)
disableAll () : undefined 从所有层中删除成员。(始终不显示该对象)

那么,现在我们要实现文字的隐藏与显示,逻辑就很简单了。

我们首先将相机、灯光设置enableAll ,让他们在所有图层都显示,然后将地球的标签和简介文字图层分别使用set方法设置为0和1,然后,我们使用toggle 来切换相机的图层就能实现不同文字的显示与隐藏了!

代码实现

function init() {
// 定义相机和场景
// ...
camera.layers.enableAll();
// 添加光源和坐标轴助手     
// ...
dirLight.layers.enableAll();
// 创建地球对象,并设置材质
// ...
earth.layers.enableAll();
//....
earthLabel.layers.set(0);
//...
earthMassLabel.layers.set(1);
//....
}

为了方便的实现相机图层的切换,我们就不写按钮了,我们直接引入threejs自带的工具库实现切换按钮

GUI库通常用于创建用于调整 Three.js 场景中参数的调试工具栏。通过 GUI 库,你可以很方便地添加各种控件,如滑块、复选框、下拉菜单等,用于动态地调整场景中的相机位置、光照参数、材质属性等,从而更直观地查看和调试场景。

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let gui
const layers = {
'Toggle Name': function () {
camera.layers.toggle(0);
},
'Toggle Mass': function () {
camera.layers.toggle(1);
},
'Enable All': function () {
camera.layers.enableAll();
},
'Disable All': function () {
camera.layers.disableAll();
}
};
function init() {
// ...
initGui();
}
function initGui() {
gui = new GUI();
gui.title('Camera Layers');
gui.add(layers, 'Toggle Name');
gui.add(layers, 'Toggle Mass');
gui.add(layers, 'Enable All');
gui.add(layers, 'Disable All');
gui.open();
}

现在,我们在看看效果

Threejs中三维物体和HTML的爱恨情仇:CSS2DRenderer

丝滑,非常nice

完整代码

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>three.js css2d - label</title>
<link type="text/css" rel="stylesheet" href="main.css">
<style>
.label {
color: #FFF;
font-family: sans-serif;
padding: 2px;
background: transparent;
}
.content {
background: red;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let gui;
let camera, scene, renderer, labelRenderer;
const layers = {
'Toggle Name': function () {
camera.layers.toggle(0);
},
'Toggle Mass': function () {
camera.layers.toggle(1);
},
'Enable All': function () {
camera.layers.enableAll();
},
'Disable All': function () {
camera.layers.disableAll();
}
};
init();
function init() {
// 设置地球半径大小
const EARTH_RADIUS = 2;
// 定义相机和场景
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(10, 5, 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 earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
const textureLoader = new THREE.TextureLoader();
const earthMaterial = new THREE.MeshPhongMaterial({
specular: 0x333333,
shininess: 5,
map: textureLoader.load('./textures/earth_atmos_2048.jpg'),
specularMap: textureLoader.load('./textures/earth_specular_2048.jpg'),
normalMap: textureLoader.load('./textures/earth_normal_2048.jpg'),
normalScale: new THREE.Vector2(0.85, 0.85)
});
// 设置地球材质的贴图颜色空间为SRGB色彩空间
earthMaterial.map.colorSpace = THREE.SRGBColorSpace;
// 创建地球mesh对象
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
earth.layers.enableAll();
scene.add(earth);
const earthDiv = document.createElement('div');
earthDiv.className = 'label';
earthDiv.innerHTML = `<div>地球<span style='color:red'>简介</span></div>`;
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(1.5 * EARTH_RADIUS, 0, 0);
earthLabel.center.set(0, 1);
earth.add(earthLabel);
earthLabel.layers.set(0);
const earthMassDiv = document.createElement('div');
earthMassDiv.className = 'content';
earthMassDiv.innerHTML = `<div>重所周知,地球是地球!望周知!</div>`;
const earthMassLabel = new CSS2DObject(earthMassDiv);
earthMassLabel.position.set(1.5 * EARTH_RADIUS, 0, 0);
earthMassLabel.center.set(0, 0);
earth.add(earthMassLabel);
earthMassLabel.layers.set(1);
//
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
document.body.appendChild(labelRenderer.domElement);
const controls = new OrbitControls(camera, labelRenderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;
window.addEventListener('resize', onWindowResize);
initGui();
animate();
}
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)
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
}
function initGui() {
gui = new GUI();
gui.title('Camera Layers');
gui.add(layers, 'Toggle Name');
gui.add(layers, 'Toggle Mass');
gui.add(layers, 'Enable All');
gui.add(layers, 'Disable All');
gui.open();
}
</script>
</body>
</html>

注:图片资源需要自己引入

总结

本教程我们借助CSS2DrENDER实现了三维物体与html’的结合,现在,我们在简单回顾下核心代码

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
let labelRenderer;
init();
function init() {
// ....
const earthDiv = document.createElement('div');
earthDiv.className = 'label';
earthDiv.innerHTML = `<div>地球<span style='color:red'>简介</span></div>`;
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(1.5 * EARTH_RADIUS, 0, 0);
earthLabel.center.set(0, 1);
earth.add(earthLabel);
// ..
animate();
}
function onWindowResize() {
// ...
labelRenderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
}
</script>
</body>
</html>

各位大佬看完如果有收获,感谢点赞哈~

原文链接:https://juejin.cn/post/7355359684851580938 作者:石小石Orz

(0)
上一篇 2024年4月9日 上午10:54
下一篇 2024年4月9日 上午11:04

相关推荐

发表回复

登录后才能评论