Three.js-描边

思路

根据模型的顶点,构建线条

这是我们初始想到的办法,实际上并不合适,性能差,效果也达不到预期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 绘制模型的线框
* @param obj - 模型对象
* @returns
*/
export function renderFrameMesh(obj: Mesh) {
const edges = new EdgesGeometry(obj.geometry);
const color = new Color(1, 0, 0);
const lineBaseMaterial = new LineBasicMaterial({
color,
side: FrontSide,
linecap: 'round',
linejoin: 'round',
});
const line = new LineSegments(edges, lineBaseMaterial);

// 针对整体一个模型的情况,需要对其进行坐标转换
const objWorldPosition = new Vector3();
const objWorldQuaternion = new Quaternion();
obj.getWorldPosition(objWorldPosition);
obj.getWorldQuaternion(objWorldQuaternion);
line.position.x = objWorldPosition.x;
line.position.y = objWorldPosition.y;
line.position.z = objWorldPosition.z;
line.quaternion.x = objWorldQuaternion.x;
line.quaternion.y = objWorldQuaternion.y;
line.quaternion.z = objWorldQuaternion.z;
line.quaternion.w = objWorldQuaternion.w;
return line;
}

卷积

three.js使用卷积法实现物体描边效果 - tengge - 博客园

这个方法尝试了下,感觉还是麻烦了一些,然后发现官方居然有描边的Demo,就先放弃这个方案了。

后来发现官方的描边Demo,和这个原理是很类似的,二者的Shader代码都很像。

直接使用官方的OutlinePass

参考官方这个示例:

https://threejs.org/examples/#webgl_postprocessing_outline

这个方法是最简单的,也是我们最终使用的方案。

该方案不用我们自己手动分层处理物体,并且还有呼吸效果。

看了下源码,发现和第二种卷积的方式很类似,也是mask + edge,整体流程如下:

  • 将未选中的物体置为invisible,绘制选中物体(通过对比未选中物体的depth buffer)

  • 降低采样率到分辨率的一半

  • 将描边线Pass应用上去

  • 应用Blur效果(二分之一分辨率)

  • 应用Blur效果(四分之一分辨率)

  • 混合(Blend)

每一个操作都会执行类似这样的流程代码:

1
2
3
4
5
6
7
8
9
// fsQuard是什么:this.fsQuad = new FullScreenQuad( null );
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 );
this.edgeDetectionMaterial.uniforms[ 'visibleEdgeColor' ].value = this.tempPulseColor1;
this.edgeDetectionMaterial.uniforms[ 'hiddenEdgeColor' ].value = this.tempPulseColor2;
renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
renderer.clear();
this.fsQuad.render( renderer );

这里应用的材质都是着色器,比如edgeDetectionMaterial、separableBlurMaterial1等。

混合代码也很有参考性:

1
2
3
4
5
6
7
8
9
// Blend it additively over the input texture
this.fsQuad.material = this.overlayMaterial;
this.overlayMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskBuffer.texture;
this.overlayMaterial.uniforms[ 'edgeTexture1' ].value = this.renderTargetEdgeBuffer1.texture;
this.overlayMaterial.uniforms[ 'edgeTexture2' ].value = this.renderTargetEdgeBuffer2.texture;
this.overlayMaterial.uniforms[ 'patternTexture' ].value = this.patternTexture;
this.overlayMaterial.uniforms[ 'edgeStrength' ].value = this.edgeStrength;
this.overlayMaterial.uniforms[ 'edgeGlow' ].value = this.edgeGlow;
this.overlayMaterial.uniforms[ 'usePatternTexture' ].value = this.usePatternTexture;

思考:我们可以将常用特效,封装为Pass类进行复用。这样积累多了,我们的开发效率就上来了。并且今后还可以应用到可视化编辑器中,以类似Unity的组件化的形式附加到场景中。

如何理解WebGLRenderTarget?

官方定义:

render target is a buffer where the video card draws pixels for a scene that is being rendered in the background. It is used in different effects, such as applying postprocessing to a rendered image before displaying it on the screen.

再来看看程序中是如何初始化的:

1
2
3
4
5
6
7
8
9
10
let resx = Math.round( width / this.downSampleRatio );
let resy = Math.round( height / this.downSampleRatio )

;this.renderTargetBlurBuffer1 = new WebGLRenderTarget( resx, resy );
this.renderTargetBlurBuffer1.texture.name = 'OutlinePass.blur1';
this.renderTargetBlurBuffer1.texture.generateMipmaps = false;

this.renderTargetBlurBuffer2 = new WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ) );
this.renderTargetBlurBuffer2.texture.name = 'OutlinePass.blur2';
this.renderTargetBlurBuffer2.texture.generateMipmaps = false;

所以RenderTarget是一个GPU的帧缓冲,或者直接叫离屏渲染更好理解一些。我们可以设置其大小。

通过WebGL渲染目标WebGLRenderTarget的纹理属性.texture可以获得WebGL渲染器的渲染结果,该属性返回的结果是一个纹理对象THREE.Texture,可以作为材质Material对象颜色贴图属性map的属性。

参考资料:Three.js WebGLRenderTarget(离屏渲染)_郭隆邦技术博客的博客-CSDN博客_three. webglrendertarget

如何理解卷积?

参考:

https://www.zhihu.com/question/22298352

这就是为什么代码中有一些二分之一、四分之一的缘故。

所谓两个函数的卷积,本质上就是先将一个函数翻转,然后进行滑动叠加。

在图像处理的中,卷积处理的结果,其实就是把每个像素周边的,甚至是整个图像的像素都考虑进来,对当前像素进行某种加权处理。所以说,“积”是全局概念,或者说是一种“混合”,把两个函数在时间或者空间上进行混合。