模型描边的几种方式
在我们做3D展示时经常要显示选中效果这里就会需要对模型进行描边,然后还有的时候会有一些风格化的效果,例如显示素描绘制效果或者一些其他的艺术效果都会用到描边
既然是描边,那么问题的核心就是怎么找到那些边需要描出来,首先补充点基本知识:
3D几何体由以下基本元素组成:
顶点(Vertex):几何体上的点
边(Edge):连接两个顶点的线段
面(Face):由三个或更多顶点构成的多边形
几何体(Geometry):由面组成的完整3D形状
边来自于几何体的面与面之间的交界:
每个面都有边界
相邻面共享边
边定义了模型的轮廓和结构
那么最直观的想法是:
"既然要显示线条,那就先把所有的边都画出来吧!",这就是 WireframeGeometry 的设计思路。
几何体叠加方式
WireframeGeometry
1. 效果展示

2. 实现代码
以上图沙发实现代码为例
加载完模型后,遍历模型的里的Mesh
通过Three.js的WireframeGeometry 获取Mesh里的geometry里的边数据
// 加载sofa模型
loader.load(
'/models/sofa.glb',
(gltf) => {
const sofaModel = gltf.scene;
// 调整模型大小和位置,放在平台上,不要嵌入
sofaModel.scale.setScalar(1.2);
sofaModel.position.set(-1, 0.5, 0);
// 关闭阴影
sofaModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = false;
child.receiveShadow = false;
}
});
scene.add(sofaModel);
// 为sofa模型添加线框效果,将线框添加到模型组中
sofaModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
const wireframe = new THREE.LineSegments(
new THREE.WireframeGeometry(child.geometry),
new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 1 })
);
// 将线框添加到模型组中,这样它会跟随模型的变换
child.add(wireframe);
}
});
},
undefined,
(error) => {
console.error('sofa模型加载失败:', error);
}
);3. WireframeGeometry实现原理
// WireframeGeometry 的简化实现逻辑
class WireframeGeometry extends BufferGeometry {
constructor(geometry) {
super();
// 1. 遍历所有面
const faces = geometry.faces;
const vertices = geometry.vertices;
// 2. 提取每个面的所有边
const edges = [];
for (const face of faces) {
// 3. 添加面的三条边
edges.push(
vertices[face.a], vertices[face.b], // 边1
vertices[face.b], vertices[face.c], // 边2
vertices[face.c], vertices[face.a] // 边3
);
}
// 4. 构建线段几何体
this.setAttribute('position', new Float32BufferAttribute(edges, 3));
}
}显然这种方式满足不了我们描边的需求,显示所有网格线,线条密集,难以看清真正的轮廓。加上渲染大量不必要的线段,影响性能。
那么,分析下我们真正需要的是:
模型的轮廓边缘
重要的结构边界
能够清晰展示模型形状的线条
而不是所有的网格线
EdgesGeometry
1. 基础概念:二面角(Dihedral Angle)
EdgesGeometry 的核心是基于二面角的概念来识别边缘:

二面角:两个相邻面之间的夹角,如上图所示面A和面B的二面角就是90°
平滑表面:二面角接近 180°(π 弧度)
锐利边缘:二面角明显小于 180°
2. 实现代码
依旧以沙发为例
loader.load(
'/models/sofa.glb',
(gltf) => {
const sofaModel = gltf.scene;
sofaModel.scale.setScalar(1.2);
sofaModel.position.set(-1, 0.5, 0);
scene.add(sofaModel);
// 为sofa模型添加线框效果,将线框添加到模型组中
sofaModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
// 保持材质可见,设置淡淡的灰白色
if (child.material) {
child.material.transparent = true;
child.material.opacity = 0.3; // 30%不透明度
child.material.color.setHex(0xf0f0f0); // 淡灰白色
}
thresholdAngle = 1
const edges = new THREE.LineSegments(
new THREE.EdgesGeometry(child.geometry, thresholdAngle),
new THREE.LineBasicMaterial({
color: 0xffffff, // 白色线条
linewidth: 1,
transparent: true,
opacity: 1.0
})
);
// 将线框添加到模型组中,这样它会跟随模型的变换
child.add(edges);
}
});
},
undefined,
(error) => {
console.error('sofa模型加载失败:', error);
}
);3. 效果展示
thresholdAngle == 1
thresholdAngle == 4

thresholdAngle == 10


从效果上看对于棱角明显的物理效果比较显著,二对于复杂表面的模型边缘就过于密集了
4. EdgesGeometry实现原理
// Three.js 内部的简化实现逻辑
class EdgesGeometry extends BufferGeometry {
constructor(geometry, thresholdAngle = 1) {
super();
// 1. 获取几何体的面信息
const faces = geometry.faces;
const vertices = geometry.vertices;
// 2. 构建面的邻接关系图
const faceAdjacency = this.buildFaceAdjacency(faces);
// 3. 遍历每个面,检查与相邻面的角度
const edges = [];
for (let faceIndex = 0; faceIndex < faces.length; faceIndex++) {
const adjacentFaces = faceAdjacency[faceIndex];
for (const adjacentFaceIndex of adjacentFaces) {
const face1 = faces[faceIndex];
const face2 = faces[adjacentFaceIndex];
// 4. 计算二面角
const angle = this.calculateDihedralAngle(face1, face2, vertices);
// 5. 如果角度小于阈值,添加边缘
if (angle < thresholdAngle) {
const edge = this.extractEdge(face1, face2);
edges.push(edge);
}
}
}
// 6. 构建最终的线段几何体
this.setAttribute('position', new Float32BufferAttribute(edges, 3));
}
}后处理方式
后处理方式是在渲染完成后,通过着色器对图像进行处理
OutlinePass
OutlinePass 是 Three.js 中一个强大的后处理轮廓描边效果,它能够为3D模型添加智能的轮廓线条,实现类似卡通渲染或技术图纸的视觉效果。使用后整个场景变暗可以看看这篇文章
1. 效果展示:


可见边缘:青色描边(#00ffff)
隐藏边缘:红色描边(#ff0000)
边缘强度:5.0,厚度:2.0
支持动态对象选择,只对指定对象进行描边
2. 实现代码
核心导入和引用
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 引用管理
const composerRef = useRef<EffectComposer | null>(null);
const outlinePassRef = useRef<OutlinePass | null>(null);后处理渲染管线构建
// 创建后处理合成器
const composer = new EffectComposer(renderer);
// 基础渲染通道
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 轮廓描边通道
const outlinePass = new OutlinePass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
scene,
camera
);
composer.addPass(outlinePass);描边参数配置
// 核心参数配置
outlinePass.edgeStrength = 5.0; // 边缘强度
outlinePass.edgeGlow = 0.0; // 边缘发光
outlinePass.edgeThickness = 2.0; // 边缘厚度
outlinePass.pulsePeriod = 0; // 脉冲周期
outlinePass.visibleEdgeColor.set(0x00ffff); // 可见边缘颜色
outlinePass.hiddenEdgeColor.set(0xff0000); // 隐藏边缘颜色对象选择和描边应用
// 存储描边对象
const outlineObjects: THREE.Mesh[] = [];
// 添加基础几何体
outlineObjects.push(cube);
outlineObjects.push(sphere);
outlineObjects.push(torus);
// 动态加载模型并添加到描边列表
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
outlineObjects.push(child);
}
});
// 应用描边对象
outlinePass.selectedObjects = outlineObjects;渲染循环
const animate = () => {
requestAnimationFrame(animate);
if (controlsRef.current) {
controlsRef.current.update();
}
// 关键:使用composer.render()进行后处理渲染
composer.render();
};3. 实现原理
见我的这篇文章