之前实现的文字是 DOM+CSS 的结合方案,那其实在旋转或者缩放视角的时候就会出现错位,需要重新计算屏幕坐标,而且跟 3d 空间结合的不是很顺畅。我们可以用贴图+PlaneGeometry+canvas 动态生成纹理的方式来绘制路标
路标
让 ui 出点 3d 模型,直接导入场景里是最快的办法了。也可以用贴图+PlaneGeometry,又或者时间精力充裕的话,用 Shape+ExtrudeGeometry 来封装一系列的路面标识。下面以直行箭头为例看看分别该怎么实现:
PlaneGeometry
这里贴图可以>=2x 的图像避免模糊,现在大部分设备的像素比至少有 2(window.devicePixelRatio)
const geometry = new THREE.PlaneGeometry(1, 1);
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("path/to/sign.png");
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
});
const sign = new THREE.Mesh(geometry, material);
sign.rotation.x = -Math.PI / 2;
scene.add(sign);
另外其实可以针对贴图做 LOD 优化,距离相机较远的加载低分辨率图片即可
function updateTextureResolution(camera) {
objects.forEach((obj) => {
const distance = obj.position.distanceTo(camera.position);
obj.material.map = distance > 10 ? lowResTexture : highResTexture;
});
}
ExtrudeGeometry
限速文字
这类文字有好几种,其实简单点直接贴图就搞定了,但后面想换颜色或者改字体,又或者想加一些其他文字提示,就显得有点不灵活了。那直接用 3d 文字?比如之前的 fontLoader+TextGeometry 的方法来做,也行吧,但这里对 3d 文字的诉求并不高,只需要一个平面,暂时没有厚度需求。所以其实这个可以用 canvas 来动态地生成文字贴图
canvas 生成动态纹理函数
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.fillText("Hello Three.js", 10, 50);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.MeshBasicMaterial({ map: texture });
const plane = new THREE.Mesh(new THREE.PlaneGeometry(), material);
- 文字支持多种?
- 缩放模糊咋解决?
- 其他方案(SpriteMaterial)
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
transparent: true,
});
const sprite = new THREE.Sprite(spriteMaterial);
路标重叠
那如果路面上的文字或路标元素不小心重叠了咋办,虽然实际道路不会存在这种情况,但万一某天我想在道路上写个广告呢(坏笑)。这里其实就可以做个碰撞检测,简单点用 AABB 的包围盒来探测,threejs 其实自带了,因为我们用的是平面几何体,所以其实很好检测的(如果是 3d 模型的路标,还需要额外做一层包围盒)
// 创建两个平面几何体
const plane1 = new THREE.Mesh(new THREE.PlaneGeometry(5, 5), material);
const plane2 = new THREE.Mesh(new THREE.PlaneGeometry(5, 5), material);
// 初始化包围盒
const box1 = new THREE.Box3().setFromObject(plane1);
const box2 = new THREE.Box3().setFromObject(plane2);
// 每帧更新包围盒位置并检测碰撞
function animate() {
requestAnimationFrame(animate);
// 更新包围盒(需在物体移动后调用)
box1.setFromObject(plane1);
box2.setFromObject(plane2);
// 检测碰撞
if (box1.intersectsBox(box2)) {
// 碰撞时触发透明度变化
setPlaneOpacity(plane1, 0.5); // 将plane1透明度设为50%
}
}
// 初始化材质时启用透明支持
const material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 1.0, // 初始不透明
});
// 动态调整透明度的函数
function setPlaneOpacity(plane, opacity) {
plane.material.opacity = opacity;
plane.material.needsUpdate = true; // 强制材质更新
}
【补个广告文字+路标碰撞检测的 gif,引流】
最后
不过 u1s1,这类硬广总会让人诟病,您觉得呢?