【ThreeJs】利用Depth Peel技术,解决模型半透明出现的碎三角面问题

在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 )
}

大功告成!最后效果如下:

开启 depth peel 前后对比

在线DEMO

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值