简介
在threejs的开发中,我们经常会遇到三维物体附近需要渲染html标签的情况,比如下图:
其实,实现这样一个需求非常简单!我们只需要了解CSS2DObject 和 CSS2DRenderer两个概念即可。接下来,我们将借助CSS2DRenderer实现下面的效果:
核心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场景,展示了一个带有纹理的地球模型,并实现基本的交互和窗口自适应功能。我们看看代码实现的效果:
现在,我们借助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>
现在,我们就实现了如图的效果
如何隐藏与显示文字
要想实现文字的显示与隐藏功能,我们必须了解一个概念:Layers
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();
}
现在,我们在看看效果
丝滑,非常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>
各位大佬看完如果有收获,感谢点赞哈~