three中的标签一般是根据物体进行定位,比如这样:three-label例子
但是在VR开发中,由于VR场景是通过贴图来模拟出伪VR效果,并没有实体在场景中,在实际开发中很不好定位,所以要想一些其他方案来在场景中定位标注,通过归纳,大致有以下三种方案:
方案一 : 使用three自带的sprite类
首先使用TextureLoader读取图片材质,注意尽量不要读取jpg或png这种标量图,最好使用svg这种矢量图,不会失真,另外svg可以使用xml来描述,后期文字样式这类信息也可以由前端来控制
可以看出在同样的缩放比例下二者清晰度有较大差异.
导入图片之后创建材质, 设置精灵纹理贴图,生成Sprite对象. three中的sprite对象,始终朝向相机,且不会随场景变化产生透视效果. 但是此时Sprite对象的中心点在其几何中心, 而非底部, 这样在场景拖放时,标注会与标注物体发生偏移. 那么更改标注几何中心的方法是将sprite.center设置成一个新的二维向量, 例如sprite.center.set(0.5, 0), 这是简易写法, 其实是一个THREE.Vector2({x:0.5,y:0})向量赋给了sprite.center属性. 之后再设置scale属性, 由于使用的是svg贴图, 因此对scale属性设置不会对清晰度造成影响.
伪代码:
ThreeVR.prototype.renderSVGLabels = function () {
const label = {
img: require("@vr/2.svg"),
x: 0,
y: 1,
z: 50
}
const {
img,
x,
y,
z
} = label
// 读取材质,使用svg不会失真
const texture = new THREE.TextureLoader().load(img);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture, // 设置精灵纹理贴图
});
// three的sprite对象,始终朝向相机,且不会随场景变化产生透视效果
const sprite = new THREE.Sprite(spriteMaterial);
// 中心点设置成sprite底部,这其实是一个THREE.Vector2({x:0.5,y:0})向量
sprite.center.set(0.5, 0)
// x,y轴放大10倍,由于一直面向前前方,z轴不需要放大
sprite.scale.set(10, 10, 1);
sprite.position.set(x, y, z)
this.scene.add(sprite);
}
方案二 : 使用CSS2DRenderer渲染成2D标签
这种标签显示效果与第一种完全一致, 优点是使用css直接渲染成dom, 文字和样式可以完全由前端控制, 劣势是在渲染时由于要实时计算标签dom位置, 切换时要加载与卸载dom元素,比较浪费性能,但是经过测算,一百个之内的dom加载不会超过60fps, 不会有肉眼可见的卡顿现象, 在可接受范围之内,相当多的竞品也是使用的该方案
首先要引入CSS2DRenderer库的 CSS2DRenderer, CSS2DObject两个构造函数
在场景中选定一个定位点作为标签位置, 然后新建一个dom, 在这个定位点用2D渲染器成标签,同时这个标签也可以引入onclick 事件, 这个事件可以被dom响应,也可以在vuex中捕获该事件,将点击事件注册到vuex中,最后将CSS2DRenderer挂载到dom中,再更新相机投影矩阵即可, js逻辑不复杂, 这种技术的难点在于css编写
css中,我首先新建了一个div,class为talent-labels,这是一个0*0的容器,在three中用于被定位, 接收三维坐标, 它的内部是一个class为talent-label-texts的div,这个div用于调整标签的中心点,由于这个标签默认中心点在左上方,我们使用 position: absolute; transform: translate(-50%, -100%);把中心点调整到底部中央. talent-label-texts里面有三个div分别是text,line,和ball用于展示,注意这些元素必须是 position: relative;display: block; 不能脱离父元素的css流.这样一个会随场景移动的标签就制作好了
.talent-labels接收接收三维坐标
.talent-labels {
//接收三维坐标
color: #fff;
font-size: 15px;
cursor: pointer;
}
.talent-label-texts把中心点调整到底部中央
.talent-labels .talent-label-texts {
//把中心点调整到底部中央
position: absolute;
transform: translate(-50%, -100%);
height: auto;
}
下面是标签的css样式,可根据实际去处理
.talent-labels .talent-label-texts .text {
width: 20px;
border-radius: 5px;
padding: 5px;
background-color: rgba(0, 0, 0, 0.5);
}
.talent-labels .talent-label-texts .line {
display: block;
position: relative;
height: 30px;
width: 6px;
margin-left: 12px;
background-color: rgba(0, 0, 0, 0.5);
}
.talent-labels .talent-label-texts .ball {
display: block;
background-color: rgba(255, 0, 0, 0.5);
position: relative;
border-radius: 50%;
width: 10px;
height: 10px;
margin-left: 10px;
background-color: rgba(0, 0, 0, 1);
}
js伪代码:
import {
CSS2DRenderer,
CSS2DObject
} from "@/utils/renderers/CSS2DRenderer.js";
this.labelRenderer = new CSS2DRenderer();
// 渲染固定位标签
ThreeVR.prototype.renderCSS2DLabels = async function (activeChooseRoom, labels) {
const that = this;
// 新建一个3D容器,用于包含标签
const tagObject = new THREE.Object3D();
this.scene.add(tagObject);
let scenelabels = [];
scenelabels = labels[activeChooseRoom];
scenelabels = [{
label: '这是一个标签',
x: 0,
y: 0,
z: 100
},
{
label: '这是另一个标签',
x: -100,
y: -5,
z: 10
}
]
if (scenelabels && scenelabels.length > 0) {
scenelabels.forEach((scenelabel) => {
const {
label,
x,
y,
z
} = scenelabel;
/**
* 新建一个dom,用2D渲染器成标签
* */
const earthDiv = document.createElement("div");
earthDiv.className = "talent-labels";
earthDiv.innerHTML = `
${label}
`;
earthDiv.onclick = function (e) {
console.log("点击了", e.target.innerHTML);
// 在vuex中捕获事件,这是一个观察者模式,将vuex的引用导入到three中,就可以将点击事件注册到vuex中
that.Vuex.dispatch("tag/setClickedTag", e.target.innerHTML);
};
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(x, y, z);
// 将标签放入3D容器,便于管理
tagObject.add(earthLabel);
});
document
.getElementById(this.canvasId)
.appendChild(this.labelRenderer.domElement);
}
// 更新相机投影矩阵,使渲染器生效
this.camera.updateProjectionMatrix()
};
当然创造了标签之后,在切换场景时还要删除标签,删除他们就是一个递归,遍历dom就好了,从尾部开始查找,反向递归比较节省性能
伪代码:
ThreeVR.prototype.clearLabels = function (className = "talent-labels") {
// 根据class名获取到dom
let labels = document.getElementsByClassName(className);
// 转化成数组
labels = Array.from(labels)
while (labels.length > 0) {
const firstLabel = labels.shift()
// 父元素
const labelsDiv = firstLabel.parentNode;
let child = null
if (labelsDiv)
child = labelsDiv.lastElementChild;
while (child) {
// 若有子元素,则递归
if (child.lastElementChild) {
this.clearLabels(child.lastElementChild.className)
}
// 若没有子元素,循环删掉最后一个
labelsDiv.removeChild(child);
child = labelsDiv.lastElementChild;
}
}
};
方案三 : 使用CSS3DRenderer渲染成3D标签
2D标签与3D标签最大的不同就是 : 3D标签在场景中会跟随相机变化而产生透视效果
可以看出在这个特殊的仰视角度下,3D标签跟随相机发生了透视,而2D标签依旧方方正正
3D代码渲染代码与2D雷同, 唯一要关注的是,3D标签不是默认朝向相机的,要多加一个rotateY属性, 这个函数接受的是角度值
伪代码:
ThreeVR.prototype.renderCSS3DLabels = async function (activeChooseRoom, labels) {
const that = this;
// 新建一个3D容器,用于包含标签
const tagObject = new THREE.Object3D();
this.scene.add(tagObject);
let scenelabels = [];
scenelabels = labels[activeChooseRoom];
scenelabels = [{
label: '这是一个标签',
x: 0,
y: 0,
z: 100,
r: Math.PI
},
{
label: '这是另一个标签',
x: -100,
y: -5,
z: 10,
r: Math.PI / 2
}
]
if (scenelabels && scenelabels.length > 0) {
scenelabels.forEach((scenelabel) => {
const {
label,
x,
y,
z,
r
} = scenelabel;
/**
* 新建一个dom,用3D渲染器成标签
* */
const earthDiv = document.createElement("div");
earthDiv.className = "talent-labels";
earthDiv.innerHTML = `
${label}
`;
earthDiv.onclick = function (e) {
console.log("点击了", e.target.innerHTML);
// 在vuex中捕获事件,这是一个观察者模式,将vuex的引用导入到three中,就可以将点击事件注册到vuex中
that.Vuex.dispatch("tag/setClickedTag", e.target.innerHTML);
};
const earthLabel = new CSS3DObject(earthDiv);
// css3d渲染的标签不是正对着相机的,要进行角度调整
if (r)
earthLabel.rotateY(r)
earthLabel.position.set(x, y, z);
// 适配标签大小
earthLabel.scale.set(0.2, .2, .2)
tagObject.add(earthLabel);
});
document
.getElementById(this.canvasId)
.appendChild(this.labelRenderer3.domElement);
}
// 更新相机投影矩阵,使渲染器生效
this.camera.updateProjectionMatrix()
};
最后,若使用camera.lookAt(scene.position)看向场景, 记得不要把camera放在(0,0,0),这样会使controls失效,要让camera有一个小的偏移
伪代码:
ThreeVR.prototype.initCamera = function () {
this.camera.lookAt(this.scene.position);
this.camera.position.x = -0.16;
this.camera.position.y = 0.1;
this.camera.position.z = 0.1;
};