在我们做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. 实现原理

见我的这篇文章