几何顶点变换
这是一个可以直接在 VitePress 页面中运行的 Three.js 示例。上方显示运行效果,下方展示对应源码。
运行效果
原理
Three.js 中的几何体顶点数据保存在 geometry.attributes.position 中。mesh.position、mesh.rotation、mesh.scale 改变的是对象自身的变换矩阵,而 geometry.translate()、geometry.rotateX()、geometry.scale() 改变的是几何体内部的顶点坐标。
这意味着几何顶点变换会真正改写几何体数据。示例中每次调整 GUI 都会从原始几何体复制一份顶点数据,然后依次应用位移、旋转、缩放,并重新计算法向量和顶点颜色。
示例中的黄色小球表示顶点位置,顶点颜色会根据当前顶点坐标变化,方便观察顶点数据被变换后的分布。
源码
js
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
const canvas = document.querySelector('canvas')
const demo = canvas.parentElement
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.8, 6)
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
})
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setSize(window.innerWidth, window.innerHeight)
const ambientLight = new THREE.AmbientLight('#ffffff', 0.8)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight('#ffffff', 1.6)
directionalLight.position.set(4, 6, 5)
scene.add(directionalLight)
const baseGeometry = new THREE.BoxGeometry(1.4, 1.4, 1.4, 2, 2, 2)
const geometry = baseGeometry.clone()
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.45,
metalness: 0.08
})
const wireframeMaterial = new THREE.MeshBasicMaterial({
color: '#e5e7eb',
wireframe: true,
transparent: true,
opacity: 0.28
})
const mesh = new THREE.Mesh(geometry, material)
const wireframeMesh = new THREE.Mesh(geometry, wireframeMaterial)
scene.add(mesh)
scene.add(wireframeMesh)
const vertexMarkerGeometry = new THREE.SphereGeometry(0.045, 12, 8)
const vertexMarkerMaterial = new THREE.MeshBasicMaterial({ color: '#facc15' })
const vertexMarkers = []
const vertexMarkerGroup = new THREE.Group()
scene.add(vertexMarkerGroup)
const gridHelper = new THREE.GridHelper(7, 7)
scene.add(gridHelper)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
const guiState = {
translateX: 0,
translateY: 0,
translateZ: 0,
rotateX: 0,
rotateY: 0,
rotateZ: 0,
scaleX: 1,
scaleY: 1,
scaleZ: 1,
showVertexMarkers: true,
reset: () => {
Object.assign(guiState, {
translateX: 0,
translateY: 0,
translateZ: 0,
rotateX: 0,
rotateY: 0,
rotateZ: 0,
scaleX: 1,
scaleY: 1,
scaleZ: 1
})
gui.controllersRecursive().forEach((controller) => controller.updateDisplay())
updateGeometry()
}
}
const applyVertexColors = () => {
const position = geometry.getAttribute('position')
const colors = []
const color = new THREE.Color()
for (let index = 0; index < position.count; index += 1) {
const x = position.getX(index)
const y = position.getY(index)
const z = position.getZ(index)
// 用顶点当前位置生成颜色,便于观察几何顶点数据变换后的分布。
color.setRGB((x + 2.5) / 5, (y + 2.5) / 5, (z + 2.5) / 5)
colors.push(color.r, color.g, color.b)
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3))
}
const syncVertexMarkers = () => {
const position = geometry.getAttribute('position')
while (vertexMarkers.length < position.count) {
const marker = new THREE.Mesh(vertexMarkerGeometry, vertexMarkerMaterial)
vertexMarkers.push(marker)
vertexMarkerGroup.add(marker)
}
vertexMarkers.forEach((marker, index) => {
marker.visible = guiState.showVertexMarkers && index < position.count
if (index < position.count) {
marker.position.set(position.getX(index), position.getY(index), position.getZ(index))
}
})
}
const updateGeometry = () => {
geometry.copy(baseGeometry)
// 这里改变的是几何体顶点数据,不是 mesh.position、mesh.rotation 或 mesh.scale。
geometry.translate(guiState.translateX, guiState.translateY, guiState.translateZ)
geometry.rotateX(THREE.MathUtils.degToRad(guiState.rotateX))
geometry.rotateY(THREE.MathUtils.degToRad(guiState.rotateY))
geometry.rotateZ(THREE.MathUtils.degToRad(guiState.rotateZ))
geometry.scale(guiState.scaleX, guiState.scaleY, guiState.scaleZ)
geometry.computeVertexNormals()
applyVertexColors()
syncVertexMarkers()
}
const gui = new GUI({
title: '顶点变换',
autoPlace: false
})
gui.domElement.classList.add('three-gui')
demo.appendChild(gui.domElement)
const translateFolder = gui.addFolder('位移')
translateFolder.add(guiState, 'translateX', -2, 2, 0.05).name('X').onChange(updateGeometry)
translateFolder.add(guiState, 'translateY', -2, 2, 0.05).name('Y').onChange(updateGeometry)
translateFolder.add(guiState, 'translateZ', -2, 2, 0.05).name('Z').onChange(updateGeometry)
const rotateFolder = gui.addFolder('旋转')
rotateFolder.add(guiState, 'rotateX', -180, 180, 1).name('X').onChange(updateGeometry)
rotateFolder.add(guiState, 'rotateY', -180, 180, 1).name('Y').onChange(updateGeometry)
rotateFolder.add(guiState, 'rotateZ', -180, 180, 1).name('Z').onChange(updateGeometry)
const scaleFolder = gui.addFolder('缩放')
scaleFolder.add(guiState, 'scaleX', 0.2, 2.5, 0.05).name('X').onChange(updateGeometry)
scaleFolder.add(guiState, 'scaleY', 0.2, 2.5, 0.05).name('Y').onChange(updateGeometry)
scaleFolder.add(guiState, 'scaleZ', 0.2, 2.5, 0.05).name('Z').onChange(updateGeometry)
gui
.add(guiState, 'showVertexMarkers')
.name('显示顶点')
.onChange(syncVertexMarkers)
gui.add(guiState, 'reset').name('重置')
updateGeometry()
function animate() {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
animate()