在ThreeJS中,模型半透明设置只需要很简单的两行代码
curMat.opacity = 0.5 // 50%透明度
curMat.transparent = curMat.opacity < 1 // transparent属性必须为True才能半透明
然而.....
我们的模型有时候出现这种情况,正面看半透明效果似乎正常,
一旦旋转到侧面,就会发现大量的碎三角面片(我的妈呀,这是什么鬼?!!)
开始以为是模型数据问题,经过排查发现并非如此,这个半透明不是材质本身问题,而是绘制顺序。在OpenGL渲染中的解决方案是开启 depth peeling,但素(41's tone),threejs中并没有这个选项,需要自己实现。
实现基本原理ShrekShao是芥末解释的
The idea is straight forward as shown by the name: we draw the objects multiple times. For each pass, we peel one layer from front and one layer from back (front or back is purely depending on their depth, not if it’s from front face or back face).
主要实现函数如下:
1. depthOnbeforeCompile
depthOnbeforeCompile(shader) {
shader.uniforms.uScreenSize = this.globalPeelUniforms.uScreenSize
shader.uniforms.uPrevDepthTexture = this.globalPeelUniforms.uPrevDepthTexture
shader.uniforms.uLayer = this.globalPeelUniforms.uLayer
shader.uniforms.uDepthOffset = this.globalPeelUniforms.uDepthOffset
shader.uniforms.uDepthPeel = this.globalPeelUniforms.uDepthPeel
shader.fragmentShader = `
uniform vec2 uScreenSize;
uniform sampler2D uPrevDepthTexture;
uniform int uLayer;
uniform int uDepthPeel;
uniform float uDepthOffset;
${shader.fragmentShader}
`
//peel depth
shader.fragmentShader = shader.fragmentShader.replace(
/}$/gm,
`
if(uDepthPeel == 0) return;
if(uLayer != 0 ){
vec2 screenPos = gl_FragCoord.xy * uScreenSize;
float prevDepth = unpackRGBAToDepth(texture2D(uPrevDepthTexture,screenPos));
if(prevDepth + uDepthOffset - gl_FragCoord.z >= 0. ){
discard;
}
}
}
`
)
}
该函数在深度材质shader编译前(THREE.MeshDepthMaterial)和原始材质shader编译前中调用
this.depthMaterial = new THREE.MeshDepthMaterial()
this.depthMaterial.side = this.getSide()
this.depthMaterial.depthPacking = THREE.RGBADepthPacking
this.depthMaterial.onBeforeCompile = this.depthOnbeforeCompile.bind(this)
2. colorOnBeforeCompile
colorOnBeforeCompile(shader){
shader.fragmentShader = shader.fragmentShader.replace('#include <packing>','')
shader.fragmentShader = `
#include <packing>
uniform sampler2D uPrevColorTexture;
${shader.fragmentShader}
`
//this has early return
this.depthOnbeforeCompile(shader)
shader.fragmentShader = shader.fragmentShader.replace(
/}$/gm,
`
gl_FragColor.xyz *= gl_FragColor.a;
}
`
)
}
该函数在原始材质shader编译前调用
attach(node){
if (node.isMesh) {
node.renderOrder = 1
this.transparentObjects.push(node) // 索引,方便快速调用
node.material.onBeforeCompile = this.colorOnBeforeCompile.bind(this);
}
}
3.全局参数设置
this.globalPeelUniforms = {
uLayer: { value: 0 },
uPrevDepthTexture: { value: null },
uPrevColorTexture: { value: null },
uScreenSize: { value: new THREE.Vector2(1,1) },
uDepthPeel: { value: Number(this.options.enabled) },
uDepthOffset: { value: 0 },
}
值得注意的是,this.globalPeelUniforms.uDepthPeel.value 作为是否开启半透明开关,而不是用threejs 默认的curMat.transparent 属性。并且,curMat.transparent 必须设置为false,透明度依然由curMat.opacity控制。
4. 最后使用depth peel的render 替换threejs默认的renderer.render()
render(renderer, scene, camera){
//clear main frame
renderer.setClearColor(0x000,1)
renderer.clear()
this.globalPeelUniforms.uLayer.value = 0
//render first depth
scene.overrideMaterial = this.depthMaterial
renderer.setClearColor(0xffffff,1)
renderer.render( scene, camera, this.targets[0], true)
//first color
scene.overrideMaterial = null
renderer.setClearColor(0x000,0)
renderer.render( scene, camera, this.targets[2], true)
for( let i = 0 ; i < this.options.layers ; i ++ ){
const a = i % 3 //shift these around
const b = (i+1) % 3
const c = (i+2) % 3
const d = 3 //peel into this
this.globalPeelUniforms.uPrevDepthTexture.value = this.targets[a]
this.globalPeelUniforms.uLayer.value = i + 1
//render next depth
scene.overrideMaterial = this.depthMaterial
renderer.setClearColor(0xffffff,1)
renderer.render( scene, camera, this.targets[b], true)
//peel
scene.overrideMaterial = null
renderer.setClearColor(0x000,0)
renderer.render( scene, camera, this.targets[d], true)
//combine
this.compositeMaterial.uniforms.uTextureA.value = this.targets[c]
this.compositeMaterial.uniforms.uTextureB.value = this.targets[d]
renderer.render( this.compositeScene, camera, this.targets[a], true)
}
//render final result over opaque objects
this.globalPeelUniforms.uPrevDepthTexture.value = null
this. transparentObjects.forEach(o=>o.visible = false)
renderer.render( scene, camera )
renderer.render( this.compositeScene, camera )
this.transparentObjects.forEach(o=>o.visible = true)
//renderer.render( this.debugScene , this.debugCamera )
}
大功告成!最后效果如下:
在线DEMO