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

简介: 【8月更文挑战第7天】Threejs中三维物体和HTML的爱恨情仇:CSS2DRenderer

简介

在threejs的开发中,我们经常会遇到三维物体附近需要渲染html标签的情况,比如下图:
image.png
其实,实现这样一个需求非常简单!我们只需要了解CSS2DObjectCSS2DRenderer两个概念即可。接下来,我们将借助CSS2DRenderer实现下面的效果:
Untitled.gif

核心API

要想实现三维物体与HTML的结合,我们必须熟悉两个概念:CSS2DRenderer和CSS2DObject。它们之间的关系也很纯粹: 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场景,展示了一个带有纹理的地球模型,并实现基本的交互和窗口自适应功能。我们看看代码实现的效果:Untitled.gif
现在,我们借助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>

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

如何隐藏与显示文字

要想实现文字的显示与隐藏功能,我们必须了解一个概念: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();

}

现在,我们在看看效果
Untitled.gif
丝滑,非常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>

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

相关文章
|
2月前
|
XML 前端开发 JavaScript
Html:CSS介绍
Html:CSS介绍
50 1
|
2月前
|
前端开发
Html:CSS的书写位置
Html:CSS的书写位置
45 0
|
10天前
|
移动开发 前端开发 JavaScript
[HTML、CSS]细节与使用经验
本文总结了前端开发中的一些重要细节和技巧,包括CSS选择器、定位、层级、全局属性、滚轮控制、轮播等。作者以纯文字形式记录,便于读者使用<kbd>Ctrl + F</kbd>快速查找相关内容。文章还提供了示例代码,帮助读者更好地理解和应用这些知识点。
33 1
[HTML、CSS]细节与使用经验
|
12天前
|
移动开发 前端开发 JavaScript
[HTML、CSS]知识点
本文涵盖前端知识点扩展、HTML标签(如video、input、canvas)、datalist和details标签的使用方法,以及CSS布局技巧(如margin、overflow: hidden和动态height)。文章旨在分享作者的学习经验和实用技巧。
26 1
[HTML、CSS]知识点
|
1月前
|
前端开发 JavaScript 搜索推荐
打造个人博客网站:从零开始的HTML和CSS之旅
【9月更文挑战第32天】在这个数字化的时代,拥有一个个人博客不仅是展示自我的平台,也是技术交流的桥梁。本文将引导初学者理解并实现一个简单的个人博客网站的搭建,涵盖HTML的基础结构、CSS样式的美化技巧以及如何将两者结合来制作一个完整的网页。通过这篇文章,你将学会如何从零开始构建自己的网络空间,并在互联网世界留下你的足迹。
|
7天前
|
移动开发 JavaScript 前端开发
html table+css实现可编辑表格的示例代码
html table+css实现可编辑表格的示例代码
|
3天前
|
前端开发 JavaScript
用HTML CSS JS打造企业级官网 —— 源码直接可用
必看!用HTML+CSS+JS打造企业级官网-源码直接可用,文章代码仅用于学习,禁止用于商业
27 1
|
8天前
|
前端开发 JavaScript 安全
HTML+CSS+JS密码灯登录表单
通过结合使用HTML、CSS和JavaScript,我们创建了一个带有密码强度指示器的登录表单。这不仅提高了用户体验,还帮助用户创建更安全的密码。希望本文的详细介绍和代码示例能帮助您在实际项目中实现类似功能,提升网站的安全性和用户友好性。
19 3
|
18天前
|
前端开发 JavaScript
Canvas三维变化背景动画HTML源码
Canvas三维变化背景动画HTML源码
22 5