Skip to content

边缘几何体与线框几何体

这是一个可以直接在 VitePress 页面中运行的 Three.js 示例。上方显示运行效果,下方展示对应源码。

运行效果

原理

EdgesGeometry 会从已有几何体中提取边缘线。它会根据相邻面的夹角判断哪些边需要显示,适合用来突出模型轮廓和硬边。第二个参数 thresholdAngle 用来控制提取边缘的阈值,角度越小,显示的边通常越多。

WireframeGeometry 会把三角面片的边全部转成线段,更适合观察模型的网格拓扑结构。它显示的信息更密集,但不一定适合表现外轮廓。

这个示例使用和 GLTFLoader 和 DRACOLoader 页面一致的模型列表,加载模型后遍历其中的 Mesh,并为每个 Mesh 生成 EdgesGeometryWireframeGeometry 或两者的线段效果。

源码

js
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'

const canvas = document.querySelector('canvas')
const demo = canvas.parentElement

// 和 GLTFLoader 示例保持一致,模型统一来自 public/models。
const modelOptions = [
	{ name: 'ironman_mk44.glb', path: '/models/ironman_mk44.glb' },
	{ name: 'porsche_911.glb', path: '/models/porsche_911.glb' },
	{ name: 'drogon.glb', path: '/models/drogon.glb' },
	{ name: 'gun.glb', path: '/models/gun.glb' },
	{ name: 'city_pack.glb', path: '/models/city_pack.glb' }
]

const scene = new THREE.Scene()
scene.background = new THREE.Color('#111827')

const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 2.5, 6)

const renderer = new THREE.WebGLRenderer({
	canvas,
	antialias: true
})

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.setSize(window.innerWidth, window.innerHeight)

const ambientLight = new THREE.AmbientLight('#ffffff', 0.9)
scene.add(ambientLight)

const directionalLight = new THREE.DirectionalLight('#ffffff', 2)
directionalLight.position.set(4, 6, 5)
scene.add(directionalLight)

const gridHelper = new THREE.GridHelper(8, 8)
scene.add(gridHelper)

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')

const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)

let model = null
const overlayLines = []
const modelMaterials = new Set()

const guiState = {
	model: modelOptions[0].name,
	mode: '边缘几何体',
	thresholdAngle: 30,
	lineColor: '#facc15',
	showModel: true,
	autoRotate: true
}

const clearOverlayLines = () => {
	while (overlayLines.length > 0) {
		const line = overlayLines.pop()

		line.parent?.remove(line)
		line.geometry.dispose()
		line.material.dispose()
	}
}

const createLineSegments = (geometry, color) => new THREE.LineSegments(
	geometry,
	new THREE.LineBasicMaterial({
		color,
		toneMapped: false
	})
)

const createOverlayLines = () => {
	clearOverlayLines()

	if (!model) {
		return
	}

	model.traverse((object) => {
		if (!object.isMesh || !object.geometry) {
			return
		}

		if (guiState.mode === '边缘几何体' || guiState.mode === '同时显示') {
			// EdgesGeometry 只提取夹角超过 thresholdAngle 的轮廓边,更适合观察模型外形。
			const edgesGeometry = new THREE.EdgesGeometry(object.geometry, guiState.thresholdAngle)
			const edgesLine = createLineSegments(edgesGeometry, guiState.lineColor)

			object.add(edgesLine)
			overlayLines.push(edgesLine)
		}

		if (guiState.mode === '线框几何体' || guiState.mode === '同时显示') {
			// WireframeGeometry 会把三角面片的边全部画出来,更适合观察网格拓扑结构。
			const wireframeGeometry = new THREE.WireframeGeometry(object.geometry)
			const wireframeLine = createLineSegments(wireframeGeometry, '#38bdf8')

			object.add(wireframeLine)
			overlayLines.push(wireframeLine)
		}
	})
}

const loadModel = (modelName) => {
	const option = modelOptions.find((item) => item.name === modelName)

	if (!option) {
		return
	}

	gltfLoader.load(option.path, (gltf) => {
		model = gltf.scene
		scene.add(model)
		createOverlayLines()
	})
}

const gui = new GUI({
	title: '几何线框控制',
	autoPlace: false
})

gui.domElement.classList.add('three-gui')
demo.appendChild(gui.domElement)

gui.add(guiState, 'model', modelOptions.map((item) => item.name)).name('模型').onChange(loadModel)
gui.add(guiState, 'mode', ['边缘几何体', '线框几何体', '同时显示']).name('模式').onChange(createOverlayLines)
gui.add(guiState, 'thresholdAngle', 1, 90, 1).name('边缘角度').onChange(createOverlayLines)
gui.addColor(guiState, 'lineColor').name('边缘颜色').onChange(createOverlayLines)

loadModel(guiState.model)

function animate() {
	requestAnimationFrame(animate)

	if (model && guiState.autoRotate) {
		model.rotation.y += 0.006
	}

	controls.update()
	renderer.render(scene, camera)
}

animate()