有手就行?Three.js实现在三维场景中拖拽添加3D文本标签

前言

在Three.js开发中,相信大家一定遇到过这样的需求吧:给场景中的模型添加标签文本 之类的东西,用来注解当前模型或者场景的一些相关信息,又或者是需要自定义一些文本内容在三维场景中去展示吧。

下面给大家分享一下Three.js 中如何实现拖拽添加3D文本标签,并且实现文本可编辑功能

相关API使用

这里通过 CSS3DObjectCSS3DRenderer 这两个api来创建和渲染3d标签。

  1. CSS3DObject:CSS3DObject是Three.js中用于表示一个CSS元素的类。通过创建一个CSS3DObject,你可以将一个普通的DOM元素嵌入到Three.js的3D场景中。你可以通过CSS3DObject的element属性来访问和操作对应的DOM元素,比如设置样式、添加事件监听器等。CSS3DObject可以在3D空间中进行变换、旋转和缩放,与其他3D对象一起渲染在屏幕上。
  2. CSS3DRenderer:CSS3DRenderer是Three.js中用于渲染CSS3DObject的渲染器。它负责将CSS3DObject转换为在3D场景中正确显示的2D元素。CSS3DRenderer会根据CSS3DObject的位置、旋转和缩放等属性,将对应的DOM元素进行变换并渲染到屏幕上。通过使用CSS3DRenderer,你可以在Three.js的3D场景中实现与普通的HTML/CSS元素交互。

创建3D文本渲染器

通过 CSS3DRenderer 创建一个3d文本渲染器

注意:这里需要创建两个渲染 WebGLRenderer 用来渲染3d场景,CSS3DRenderer 用来渲染3D文本

js 复制代码
	// 创建渲染器
	initRender() {
		this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true }) //设置抗锯齿
		//设置屏幕像素比
		this.renderer.setPixelRatio(window.devicePixelRatio)
		//渲染的尺寸大小
		const { clientHeight, clientWidth } = this.container
		this.renderer.setSize(clientWidth, clientHeight)
		this.container.appendChild(this.renderer.domElement)

		// 创建一个CSS3DRenderer
		this.css3DRenderer = new CSS3DRenderer();
		this.css3DRenderer.setSize(clientWidth, clientHeight);
		this.css3DRenderer.domElement.style.position = 'absolute';
		this.css3DRenderer.domElement.style.pointerEvents = 'none';
		this.css3DRenderer.domElement.style.top = 0;
           this.container.appendChild(this.css3DRenderer.domElement);
	}

创建3D文本标签方法

将创建3D文本标签的方法单独抽离在一个函数中去 create3dTags , 传入三个参数 clientX (鼠标拖拽结束时的X坐标), clientY (鼠标拖拽结束时的X坐标), name(当前element-plus icon组件的name)

1.这里通过 THREE.Raycaster() 检测鼠标在三维场景中的位置(x,y,z)

2.通过Vue3 jsx语法的 render 函数和 createApp 方法 创建3D文本节点

3.通过 CSS3DObject 创建一个可以在3D场景中展示的元素

4.将获取到的鼠标坐标 (x,y,z) 赋值给 CSS3DObject 创建的文本标签

5.最后通过 scene.add(cssObject) 添加到场景中去

js 复制代码
import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
import { ElIcon, ElMessage } from 'element-plus';
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { h, createApp } from "vue";

function create3dTags(clientX, clientY, name) {

	const { clientHeight, clientWidth, offsetLeft, offsetTop } = this.container
	// 计算鼠标在屏幕上的坐标
	this.mouse.x = ((clientX - offsetLeft) / clientWidth) * 2 - 1
	this.mouse.y = -((clientY - offsetTop) / clientHeight) * 2 + 1
  const raycaster = THREE.Raycaster()
	raycaster.setFromCamera(this.mouse, this.camera);
	const intersects = raycaster.intersectObjects(this.scene.children, true);

	if (intersects.length > 0) {
		var element = document.createElement('div');
		// 创建容器标签
		const backgroundColor = 'rgba(0,127,127,' + (Math.random() * 0.5 + 0.25) + ')'
		const len = this.dragTagList.length + 1
		// 创建3d标签
		const tagvMode = createApp({
			render() {
				return (
					<div>
						<div className="element-tag"
							style={{ width: 60 + 'px', height: 40 + 'px', fontSize: 6 + 'px', color: '#ffffffbf', backgroundColor }}>
							<span className='tag-txt'>
								{`标签-${len}`}
							</span>
						</div>
						<div className='tag-icon' ><ElIcon >{h(ElementPlusIconsVue[name])}</ElIcon></div>
					</div>
				)
			}
		});

		const vNode = tagvMode.mount(document.createElement('div'))
		element.appendChild(vNode.$el)

		var cssObject = new CSS3DObject(element);
          // 获取到鼠标坐标
		const { x, y, z } = intersects[0].point
		cssObject.position.set(x, y, z);
		cssObject.scale.set(.01, .01, .01)
		this.scene.add(cssObject)

	} else {
		ElMessage.warning('当前角度无法获取鼠标位置请调整"相机角度"在添加')
	}
}

手动调用 create3dTags 方法

调用多次创建不同icon的文本标签

js 复制代码
create3dTags(100,444,"More")
create3dTags(120,644,"MuteNotification")
create3dTags(200,544,"RemoveFilled")
create3dTags(200,144,"Document")
create3dTags(150,744,"FolderAdd")
create3dTags(300,54,"KnifeFork")

定义拖拽方法实现拖拽添加

  1. 这里使用的是H5的拖拽API来实现的,dragstartdragdrop
  2. 获取到 ElementPlusIconsVue 中所有图标的 name
  3. 拖拽结束在 drop 函数中获取当前鼠标的坐标
  4. 调用 create3dTags 方法 实现拖拽创建文本标签
js 复制代码
<template>
	<el-scrollbar max-height="150px">
	  <el-row class="tag-row">
		<el-col draggable="true" v-for="tag in tagList" :key="tag.name" :span="4" @dragstart="(e) => onDragstart(e, tag)" @drag="(e) => onDrag(e)">					
		     <div class="tag-box">
			<div class="icon">
			   <el-icon size="16px">
				<component :is="tag.name"></component>
			   </el-icon>
			</div>
		   </div>
		</el-col>
	    </el-row>
    </el-scrollbar>
    <div id="model" @drop="dropEnd" ref="model" @dragover.prevent>  </div>
</template>

<script setup>
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { useMeshEditStore } from '@/store/meshEditStore'
import {ref} from 'vue'

const store = useMeshEditStore();
const tagList = computed(() => {
	const arr = []
	for (const [key] of Object.entries(ElementPlusIconsVue)) {
		arr.push({ name: key })
	}
	return arr
})

const dragTag = ref(null)


// 拖拽开始
const onDragstart = (e, tag) => {
   dragTag.value = tag.name
};
// 拖拽中
const onDrag = (event) => {
	event.preventDefault();
};

// 拖拽结束
const dropEnd = (event) => {
   const { clientX, clientY } = e
    store.modelApi.create3dTags(clientX,clientY,dragTag.value);
};
</script>

界面显示

新增标签参数更新方法,实现标签内容自定义

在标签创建成功之后,我们可能需要动态的修改文本的相关信息如:文字颜色,背景颜色,图标大小,文本高度,文本宽度等信息。

为了让这个3d文本标签更加的灵活化,这里通过新增 updateTagElement 方法来实现

  1. 通过 getObjectByProperty 传入 uuid 获取到当前文本节点的内容
  2. 通过 querySelector 获取到需要改变的元素节点信息
  3. 将传入的参数赋值给对应元素的内容
js 复制代码
import TWEEN from "@tweenjs/tween.js";
function updateTagElement(tag) {
	const object = this.scene.getObjectByProperty('uuid', tag.uuid)
	if (object) {
		const element = object.element.querySelector('.element-tag')
		const iconElement = object.element.querySelector('.tag-icon')
		const txtElement = object.element.querySelector('.tag-txt')
		const { height, backgroundColor, width,
			fontSize, color, innerText, iconColor,
			iconSize, positionY, positionX, positionZ } = tag

		element.style.height = height + 'px'
		element.style.backgroundColor = backgroundColor
		element.style.boxShadow = `0px 0px 4px ${backgroundColor}`
		element.style.width = width + 'px'
		element.style.fontSize = fontSize + 'px'
		element.style.color = color

		txtElement.innerText = innerText

		iconElement.style.fontSize = iconSize + 'px'
		iconElement.style.color = iconColor

		// 修改坐标位置
		const Tween = new TWEEN.Tween(object.position)
		const endPosition = {
			x: positionX,
			y: positionY,
			z: positionZ
		}
		Tween.to(endPosition, 500)
		Tween.onUpdate((val) => {
			object.position.set(val.x || 0, val.y || 0, val.z || 0)
		})
		Tween.start();
	}

}

效果图

带有模型的效果图

结语

ok这样一个有手就行的Three.js拖拽添加和更新3D文本标签的功能就实现了

如果你有更好的方法欢迎评论区留言交流讨论

完整的功能代码地址可参考

threejs 3d模型可视化编辑器 项目的标签配置模块的功能

gitee.com/ZHANG_6666/...

相关推荐
独立开阀者_FwtCoder19 分钟前
放弃 JSON.parse(JSON.stringify()) 吧!试试现代深拷贝!
前端·javascript·github
爱喝水的小周1 小时前
AJAX vs axios vs fetch
前端·javascript·ajax
Jinxiansen02111 小时前
unplugin-vue-components 最佳实践手册
前端·javascript·vue.js
几道之旅2 小时前
介绍electron
前端·javascript·electron
周胡杰2 小时前
鸿蒙arkts使用关系型数据库,使用DB Browser for SQLite连接和查看数据库数据?使用TaskPool进行频繁数据库操作
前端·数据库·华为·harmonyos·鸿蒙·鸿蒙系统
31535669132 小时前
ClipReader:一个剪贴板英语单词阅读器
前端·后端
玲小珑2 小时前
Next.js 教程系列(十一)数据缓存策略与 Next.js 运行时
前端·next.js
qiyue772 小时前
AI编程专栏(三)- 实战无手写代码,Monorepo结构框架开发
前端·ai编程
断竿散人2 小时前
JavaScript 异常捕获完全指南(下):前端框架与生产监控实战
前端·javascript·前端框架
Danny_FD2 小时前
Vue2 + Vuex 实现页面跳转时的状态监听与处理
前端