切换语言为:繁体
Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案

Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案

  • 爱糖宝
  • 2024-09-18
  • 2039
  • 0
  • 0

这个问题足够棘手,所以我单独写了这篇文章,从头到尾,从上层到底层,描述这个问题的前因后果。利用混合模式,自定义元素彻底解决这个问题。

如下图:使用 CSS3DRender 渲染的 CSS3DObject , 与 Mesh 的叠加,即使逻辑上 CSS3DObject 被 Mesh 遮挡了部分,但是显示下,仍然无法遮挡。

Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案

问题出现的原因

首先明白,CSS3DRenderer 渲染 CSS3DObject 的承载元素并不是在 canvas 上,而是在 CSS3DRenderer.domElement 上,该元素与 canvas 为两个独立元素, 大部分的情况下,为了能正常显示 CSS3DObject ,会将 CSS3DRenderer.domElement 层级提高,在 canvas 上层,或者在 canvas 下层,但是 CSS3DObject.element 采用相对定位, 使之依旧能展现在 canvas 上层,如此实现,导致了今天需要解决的问题。就是 CSS3DObject.element 属于 html 元素,并且覆盖在 canvas 上层,导致 canvas 内部 Object3D 无法遮挡它。

Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案 ## 如何解决这个问题

提高 canvas 层级,将 CSS3DRenderer.domElement 全覆盖

如下图:这样就解决了 CSS3DObject 对象无法被遮挡的问题,因为这样会将整个 CSS3DRenderer.domElement 遮挡。

Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案

在遮挡住了整个 CSS3DRenderer.domElement 的情况下,如何显示需要展示的 CSS3DObject ?

我们可以想象,有两张不透明的纸 A 和 B ,A 在下面,B 在上面,A 上面画了一个五角星, 如何才能看见这个五角星呢?

当然是在 B 上面对应位置,裁切一个相同的五角星区域,这样便可透过 B 看到 A 纸上面的五角星了。

Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案

这一步的描述,在代码中如何编写呢???

    const innerText = "测试文本"

    const object3d = new THREE.Object3D()

    const element = document.createElement(type);
    element.style.width = width + 'px';
    element.style.height = height + 'px';
    element.innerText = innerText

    const css3dObject = new CSS3DObject(element);

    object3d.add(css3dObject)

    // 创建一个不可见的平面,大小与 html 元素相同,混合模式设置为 NoBlending 。
    const geometry = new THREE.BoxGeometry(width, height, 1);
    const mesh = new THREE.Mesh(geometry);

    object3d.add(mesh);

可以看见这个平面与球存在遮挡关系,因为同处于 canvas ,并且具有深度信息。

Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案

我们更改一下上述代码,将平面的材质混合模式更改为 NoBlending

    // 创建一个不可见的平面,大小与 html 元素相同,混合模式设置为 NoBlending 。
    const geometry = new THREE.BoxGeometry(width, height, 1);
    const material = new THREE.MeshBasicMaterial({opacity:0.01,blending:THREE.NoBlending})
    const mesh = new THREE.Mesh(geometry);

    object3d.add(mesh);

更改了混合模式后,在 canvas 底层的 HTML 元素被映射出来。

Three.js 中 HTML 元素无法被 Object3D 对象遮挡的问题解决方案

至此,该问题被解决,但是这个问题并没有结束

问题拓展1: CSS2DObject 与 Object3D 的遮挡问题如何解决呢?

CSS2DObject 与 CSS3DObject 的区别在于,CSS2DObject 始终朝向屏幕,CSS2DObject 远近一样大。

确定了 CSS2DObject 与 CSS3DObject 的区别,那就好办了,我们只需要在相机发生改变的时候,计算,更新 CSS3DObject 的朝向和大小,就是 CSS2DObject 了。

// 该值的设置,根据需要展示的效果确定。
const __distance = 800

controls.addEventListener("change", e => {

    const direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize()

    css3ds.forEach(css3d => {

        // 计算 CSS3DObject 的大小,使之保持远近一样大
        const distance = camera.position.distanceTo(css3d.position)

        const scale = distance / __distance

        css3d.scale.set(scale, scale, scale)

        // 计算 CSS3DObject 的朝向,使之始终朝向屏幕
        const lookAt = css3d.getWorldPosition(new THREE.Vector3()).add(direction)
    
        css3d.lookAt(lookAt)

    })

})

问题拓展2: 在 canvas 中创建的平面,如何自适应获取大小?

情景:封装了一个函数,写好了确定的样式,但是 html 元素的 innerText 由函数传参确定。

这种情况下,我们无法确定创建的平面的宽和高。

    function createOcclusionCSS2DObject(innerText){
        
        const div = document.createElement("div");
        div.innerText = innerText

        // 宽度和高度如何得到??
        const geometry = new THREE.BoxGeometry(width,height) // ????? 

    }

在元素创建的时候,元素的宽度和高度都是 0,我们无法直接获取。所以创建平面的时候,不应该和元素创建同步。

需要在元素挂载在页面上的时候,我们再创建大小相同的平面。元素挂载在页面的时候,我们可以获取到元素的宽和高。

我们创建自定义元素,在元素挂载上页面的钩子函数中,创建平面。

代码上如何操作呢??很多前端开发者可能从来没有用过这个构造函数

class CustomDivElement extends HTMLElement {

    constructor(){

        super()

    }

    connectedCallback(){
        
        console.log("自定义元素加入页面")

        // 创建平面
        const width = this.clientWidth 
        const height = this.clientHeight 
        const geometry = new THREE.BoxGeometry(width,height,1)

        // 剩余逻辑,相同 ......

        this.__update()

    }

    disconnectedCallback(){
        
        console.log("自定义元素从页面移除")

    }

    adoptedCallback(){

        console.log("自定义元素转移到新页面")

    }

    attributeChangedCallback(name,oldValue,newValue){

        console.log("自定义元素属性发生变化")

        this.__update()

    }

    __update(){

        // update

    }

}

customElements.define("custom-div",CustomDivElement)

const customDivElement = document.createElement("custom-div")

至此,这个问题算是完整结束了。

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.