法線延展法
網上使用法線延展法實作物體描邊效果的文章比較多,這裡不再描述。
但是這種方法有個缺點:當兩個面的法線夾角差别較大時,兩個面的描邊無法完美連接配接。如下圖所示:
卷積法
這裡使用另一種方法卷積法實作物體描邊效果,一般機器學習使用該方法比較多。先看效果圖:
使用three.js具體的實作方法如下:
- 建立着色器材質,隐藏不需要描邊的物體進行渲染,将需要描邊的位置渲染成白色,其他位置渲染成黑色。
- 利用片源着色器計算卷積,白色是物體内部,黑色是物體外部,灰色是邊框。
- 設定材質透明、不融合,将邊框疊加到原圖上,可以使用FXAA抗鋸齒。
這三步就可以實作了,很簡單吧。下面我們将詳細介紹實作方法,不想看的可以直接去看完整實作代碼:
完整代碼: https://gitee.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/helper/SelectHelper.js
詳細的實作過程:
1. 使用three.js正常繪制場景,得到下圖,這裡不介紹了。
2. 建立着色器材質,隐藏所有不需要描邊的物體。将需要描邊的物體繪制成白色,其他地方繪制成黑色。
隐藏不需要描邊的物體後,将整個場景材質替換。
renderScene.overrideMaterial = this.maskMaterial;
着色器材質:
const maskMaterial = new THREE.ShaderMaterial({
vertexShader: MaskVertex,
fragmentShader: MaskFragment,
depthTest: false
});
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);
}
效果圖:
3. 建立着色器材質進行卷積計算,每四個像素顔色求平均值得到一個像素。描邊物體内部是白色,外部是黑色,物體邊緣處會得到灰色。灰色就是我們所需的邊框。
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的兩倍。color是邊框顔色,thickness是邊框粗細。
注意,要将材質transparent設定為true。
EdgeVertex:
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
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);
}
4. 建立着色器材質,将邊框疊加到原來的圖檔上。由于FXAA比較複雜,這裡使用簡單的疊加方法。
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,否則會把原來的圖檔覆寫掉。
CopyVertexShader:
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
CopyFragmentShader:
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
}
得到最終效果圖:
參考資料:
1. 描邊實作完整代碼:https://gitee.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/helper/SelectHelper.js
2. 基于three.js的開源三維場景編輯器:https://github.com/tengge1/ShadowEditor
3. three.js後期處理描邊:https://threejs.org/examples/#webgl_postprocessing_outline
4. 卷積工作原理:https://www.zhihu.com/question/39022858?sort=created
5. 法線延展法實作物體描邊:https://blog.csdn.net/srk19960903/article/details/73863853