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

简介

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

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

感谢各位彦祖点赞、收藏

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

其实,实现这样一个需求非常简单!我们只需要了解CSS2DObjectCSS2DRenderer两个概念即可。接下来,我们将借助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中文网

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中,如果你掌握基础,加载一个地球是非常容易得

JS 复制代码
<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场景,展示了一个带有纹理的地球模型,并实现基本的交互和窗口自适应功能。我们看看代码实现的效果:

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

给场景添加HTML介绍

JS 复制代码
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两个类名,因此我们还有加入相应的样式

JS 复制代码
<head>
    // .....
    <style>
        .label {
            color: #FFF;
            font-family: sans-serif;
            padding: 2px;
            background: transparent;
        }

        .content {
            background: red;
        }
    </style>
</head>

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

如何隐藏与显示文字

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

Layers

我们先看官网的释义

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

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

我们看一个简单示例:

JS 复制代码
// 创建一个对象
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 来切换相机的图层就能实现不同文字的显示与隐藏了!

代码实现

JS 复制代码
function init() {
    // 定义相机和场景
    // ...
    camera.layers.enableAll();

    // 添加光源和坐标轴助手     
    // ...
    dirLight.layers.enableAll();

    // 创建地球对象,并设置材质
    // ...
    earth.layers.enableAll();


    //....
    earthLabel.layers.set(0);

    //...
    earthMassLabel.layers.set(1);

    //....
}

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

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

JS 复制代码
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();

}

现在,我们在看看效果

丝滑,非常nice

完整代码

JS 复制代码
<!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'的结合,现在,我们在简单回顾下核心代码

JS 复制代码
<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>

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

相关推荐
小彭努力中1 天前
138. CSS3DRenderer渲染HTML标签
前端·深度学习·3d·webgl·three.js
优雅永不过时·1 天前
three.js实现地球 外部扫描的着色器
前端·javascript·webgl·three.js·着色器
阿铎前端5 天前
Three.js PBR材质
vue·three.js
二三ErSan7 天前
深入探索ES 3D Editor:一个基于ThreeJS + Vue 3 + Naive UI的三维编辑器
three.js
Sword999 天前
【ThreeJs原理解析】第2期 | 旋转、平移、缩放实现原理
前端·three.js·源码阅读
小白菜学前端9 天前
Threejs 材质贴图、光照和投影详解
前端·3d·three.js
破浪前行·吴10 天前
使用@react-three/fiber,@mkkellogg/gaussian-splats-3d加载.splat,.ply,.ksplat文件
前端·react.js·three.js
格瑞@_@17 天前
11.Three.js使用indexeddb前端缓存模型优化前端加载效率
前端·javascript·缓存·three.js·indexeddb缓存
谢小飞17 天前
我做了三把椅子原来纹理这样加载切换
前端·three.js
小白菜学前端18 天前
ThreeJS创建一个3D物体的基本流程
3d·three.js