这个问题足够棘手,所以我单独写了这篇文章,从头到尾,从上层到底层,描述这个问题的前因后果。利用混合模式,自定义元素彻底解决这个问题。
如下图:使用 CSS3DRender 渲染的 CSS3DObject , 与 Mesh 的叠加,即使逻辑上 CSS3DObject 被 Mesh 遮挡了部分,但是显示下,仍然无法遮挡。
问题出现的原因
首先明白,CSS3DRenderer 渲染 CSS3DObject 的承载元素并不是在 canvas 上,而是在 CSS3DRenderer.domElement 上,该元素与 canvas 为两个独立元素, 大部分的情况下,为了能正常显示 CSS3DObject ,会将 CSS3DRenderer.domElement 层级提高,在 canvas 上层,或者在 canvas 下层,但是 CSS3DObject.element 采用相对定位, 使之依旧能展现在 canvas 上层,如此实现,导致了今天需要解决的问题。就是 CSS3DObject.element 属于 html 元素,并且覆盖在 canvas 上层,导致 canvas 内部 Object3D 无法遮挡它。
## 如何解决这个问题
提高 canvas 层级,将 CSS3DRenderer.domElement 全覆盖
如下图:这样就解决了 CSS3DObject 对象无法被遮挡的问题,因为这样会将整个 CSS3DRenderer.domElement 遮挡。
在遮挡住了整个 CSS3DRenderer.domElement 的情况下,如何显示需要展示的 CSS3DObject ?
我们可以想象,有两张不透明的纸 A 和 B ,A 在下面,B 在上面,A 上面画了一个五角星, 如何才能看见这个五角星呢?
当然是在 B 上面对应位置,裁切一个相同的五角星区域,这样便可透过 B 看到 A 纸上面的五角星了。
这一步的描述,在代码中如何编写呢???
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 ,并且具有深度信息。
我们更改一下上述代码,将平面的材质混合模式更改为 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 元素被映射出来。
至此,该问题被解决,但是这个问题并没有结束
问题拓展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")