源码地址在three/examples/jsm/postprocessing/OutlinePass.js

OutlinePass 是一个后处理通道,用于在选中的3D对象周围渲染轮廓线。这就可以做对象选择时的高亮

1. 核心算法步骤如下:

步骤1:深度缓冲区渲染

// 1. Draw Non Selected objects in the depth buffer
this.renderScene.overrideMaterial = this.depthMaterial;
renderer.setRenderTarget( this.renderTargetDepthBuffer );
renderer.clear();
renderer.render( this.renderScene, this.renderCamera );
  • 先隐藏选中的对象

  • 使用 MeshDepthMaterial 渲染非选中对象到深度缓冲区

  • 深度缓冲区存储了场景中所有非选中对象的深度信息

步骤2:遮罩生成

// Make non selected objects invisible, and draw only the selected objects, by comparing the depth buffer of non selected objects
this._changeVisibilityOfNonSelectedObjects( false );
this.renderScene.overrideMaterial = this.prepareMaskMaterial;
this.prepareMaskMaterial.uniforms[ 'depthTexture' ].value = this.renderTargetDepthBuffer.texture;
  • 隐藏非选中对象,显示选中对象

  • 使用 prepareMaskMaterial 比较选中对象与深度缓冲区

  • 生成遮罩纹理,标识轮廓区域

步骤3:边缘检测

// 3. Apply Edge Detection Pass
this._fsQuad.material = this.edgeDetectionMaterial;
this.edgeDetectionMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskDownSampleBuffer.texture;
this.edgeDetectionMaterial.uniforms[ 'texSize' ].value.set( this.renderTargetMaskDownSampleBuffer.width, this.renderTargetMaskDownSampleBuffer.height );

边缘检测着色器通过比较相邻像素的差异来检测边缘:

void main() {
    vec2 invSize = 1.0 / texSize;
    vec4 uvOffset = vec4(1.0, 0.0, 0.0, 1.0) * vec4(invSize, invSize);
    vec4 c1 = texture2D( maskTexture, vUv + uvOffset.xy);
    vec4 c2 = texture2D( maskTexture, vUv - uvOffset.xy);
    vec4 c3 = texture2D( maskTexture, vUv + uvOffset.yw);
    vec4 c4 = texture2D( maskTexture, vUv - uvOffset.yw);
    float diff1 = (c1.r - c2.r)*0.5;
    float diff2 = (c3.r - c4.r)*0.5;
    float d = length( vec2(diff1, diff2) );
    // ... 边缘颜色选择逻辑
}

步骤4:模糊处理

// 4. Apply Blur on Half res
this._fsQuad.material = this.separableBlurMaterial1;
this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = this.edgeThickness;

使用可分离高斯模糊来平滑轮廓线,支持不同分辨率的多级模糊。

2. 准备着色器材质

深度准备材质 (_getPrepareMaskMaterial)

fragmentShader:
    `#include <packing>
    varying vec4 vPosition;
    varying vec4 projTexCoord;
    uniform sampler2D depthTexture;
    uniform vec2 cameraNearFar;

    void main() {
        float depth = unpackRGBAToDepth(texture2DProj( depthTexture, projTexCoord ));
        float viewZ = - DEPTH_TO_VIEW_Z( depth, cameraNearFar.x, cameraNearFar.y );
        float depthTest = (-vPosition.z > viewZ) ? 1.0 : 0.0;
        gl_FragColor = vec4(0.0, depthTest, 1.0, 1.0);
    }`

可分离模糊材质 (_getSeparableBlurMaterial)

fragmentShader:
    `#include <common>
    // ... 高斯PDF函数
    void main() {
        vec2 invSize = 1.0 / texSize;
        float sigma = kernelRadius/2.0;
        float weightSum = gaussianPdf(0.0, sigma);
        vec4 diffuseSum = texture2D( colorTexture, vUv) * weightSum;
        // ... 高斯模糊计算
    }`

从中看到的一些表象问题的原因

1. 性能开销大

  • 需要多次渲染场景(深度缓冲区、遮罩生成、边缘检测、模糊处理)

  • 多个渲染目标的内存占用

  • 复杂的后处理管线,每帧都要执行

2. 内存消耗高

this.renderTargetMaskBuffer = new WebGLRenderTarget( this.resolution.x, this.resolution.y );
this.renderTargetDepthBuffer = new WebGLRenderTarget( this.resolution.x, this.resolution.y, { type: HalfFloatType } );
this.renderTargetMaskDownSampleBuffer = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
this.renderTargetBlurBuffer1 = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
this.renderTargetBlurBuffer2 = new WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ), { type: HalfFloatType } );
  • 创建了6个渲染目标

  • 使用 HalfFloatType 增加内存占用

  • 分辨率越高,内存消耗越大