Three.js卷积法大显身手!告别断裂,轻松实现平滑3D物体描边效果
在三维场景中,物体描边效果是一种常见的视觉增强技术,广泛应用于物体选中高亮、轮廓识别等场景。本文将介绍如何使用Three.js结合卷积法实现平滑的物体描边效果。
法线延展法的局限
网上关于使用法线延展法实现物体描边的文章很多,这种方法通过沿法线方向扩展模型顶点来生成描边。然而,这种方法存在一个明显的缺陷:当两个面的法线夹角差别较大时,两个面的描边无法完美连接,产生明显的断裂感。

卷积法解决方案
卷积法是另一种实现物体描边的方法,在机器学习领域应用广泛。这种方法通过图像处理中的卷积运算来检测物体边缘,效果更加平滑自然。
最终效果预览



实现步骤
使用Three.js实现卷积描边只需三步:
- 生成掩膜纹理:隐藏不需要描边的物体,将需要描边的物体渲染成白色,其他区域渲染成黑色
- 卷积计算:通过片元着色器进行卷积计算,白色区域为物体内部,黑色为外部,灰色为边框
- 叠加边框:设置材质透明,将计算出的边框叠加到原图上
详细实现
第一步:正常绘制场景
首先使用Three.js正常绘制三维场景,这一步不做特殊处理。

第二步:创建掩膜材质
创建着色器材质,隐藏不需要描边的物体,将需要描边的物体绘制成白色。
// 设置覆盖材质
renderScene.overrideMaterial = this.maskMaterial;
掩膜顶点着色器 MaskVertex:
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
掩膜片元着色器 MaskFragment:
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
效果如下图所示,需要描边的物体显示为白色:

第三步:卷积计算边框
创建着色器材质进行卷积计算,对每四个像素的颜色求平均值得到一个像素。物体内部为白色,外部为黑色,边缘处会得到灰色的边框。
const edgeMaterial = new THREE.ShaderMaterial({
vertexShader: EdgeVertex,
fragmentShader: EdgeFragment,
uniforms: {
maskTexture: { value: this.maskBuffer.texture },
texSize: { value: new THREE.Vector2(width, height) },
color: { value: selectedColor },
thickness: { type: 'f', value: 4 },
transparent: true
},
depthTest: false
});
注意:
texSize是计算卷积的canvas宽度和高度。为了让边框更平滑,可以设置为原canvas的两倍。同时要将材质transparent设置为true。
卷积片元着色器 EdgeFragment:
uniform sampler2D maskTexture;
uniform vec2 texSize;
uniform vec3 color;
uniform float thickness;
varying vec2 vUv;
void main() {
vec2 invSize = thickness / 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));
gl_FragColor = d > 0.0 ? vec4(color, 1.0) : vec4(0.0, 0.0, 0.0, 0.0);
}
卷积计算后的边框效果:

第四步:叠加边框到原图
创建着色器材质,将卷积计算出的边框叠加到原始场景上。
const copyMaterial = new THREE.ShaderMaterial({
vertexShader: CopyVertexShader,
fragmentShader: CopyFragmentShader,
uniforms: {
tDiffuse: { value: edgeBuffer.texture },
resolution: { value: new THREE.Vector2(1 / width, 1 / height) }
},
transparent: true,
depthTest: false
});
注意:
transparent必须设置为true,否则边框会覆盖原图。
叠加片元着色器 CopyFragmentShader:
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
}
最终效果
完成上述步骤后,即可获得平滑的物体描边效果:

参考资料
Three.js后期处理描边示例:https://threejs.org/examples/#webgl_postprocessing_outline
卷积工作原理:https://www.zhihu.com/question/39022858?sort=created
法线延展法实现物体描边:https://blog.csdn.net/srk19960903/article/details/73863853
总结
卷积法相比法线延展法,能够产生更加平滑的描边效果,避免因法线夹角导致的断裂问题。通过Three.js的着色器材质和卷积运算,我们可以轻松实现这一效果,为三维场景中的物体选中、高亮显示等功能提供更好的视觉体验。