這個問題足夠棘手,所以我單獨寫了這篇文章,從頭到尾,從上層到底層,描述這個問題的前因後果。利用混合模式,自定義元素徹底解決這個問題。
如下圖:使用 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")