Three.js中实现圆柱体精准贴合位移映射地形的问题
问题:位移贴图地形上的圆柱体无法精准对齐表面
我希望这些圆柱体能够精准放置在地形表面。尝试使用不同朝向的Raycaster(射线投射器),但射线的距离和交点测量结果并不准确——应用后圆柱体直接穿过网格底部,仿佛射线检测的是未经过变换、未应用位移映射的网格。如何让检测交点与实际位移映射的地形完全对齐?
项目地址:https://github.com/ledlogic/terrain

问题原因
Three.js的Raycaster基于CPU端的原始几何体数据做射线检测,而displacementMap是在GPU渲染阶段才对顶点进行位移变换的,CPU层面的几何体顶点并没有被实际修改,所以射线检测无法识别位移后的地形高度,自然会得到错误的交点。
解决方案
方案1:手动计算位移高度(推荐,适合大网格)
直接读取位移贴图的像素值,结合displacementScale计算对应位置的地形高度,手动设置圆柱体的Y坐标。步骤如下:
- 将位移贴图的过滤方式设为
THREE.NearestFilter,避免插值导致的精度问题 - 根据圆柱体的X/Z位置,计算其在位移贴图上对应的UV坐标
- 读取该UV坐标对应的像素灰度值,转换为高度值,再结合
displacementScale计算最终地形高度 - 调整圆柱体的Y位置,使其底部贴合地形
方案2:预修改几何体顶点数据(适合静态小网格)
如果地形是静态的,可以提前将位移贴图的效果应用到几何体的顶点数据上,这样Raycaster就能基于修改后的顶点进行正确检测。但这种方法会占用更多内存,不适合高细分的大网格。
修改后的代码
/* from https://www.youtube.com/watch?v=2AQLMZwQpDo */ import './style.css' import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import * as dat from 'dat.gui' const loader = new THREE.TextureLoader() // 配置位移贴图的过滤方式,避免插值精度问题 const heightImg = loader.load('/height.png') heightImg.minFilter = THREE.NearestFilter heightImg.magFilter = THREE.NearestFilter const textureImg = loader.load('/texture.jpg') const alphaImg = loader.load('/alpha-02.png') // Debug const gui = new dat.GUI() // Canvas const canvas = document.querySelector('canvas.webgl') // Scene const scene = new THREE.Scene() // Objects const width = 3; const height = 3; const widthSegments = 1500; const heightSegments = 1500; const geometry = new THREE.PlaneBufferGeometry(width, height, widthSegments, heightSegments) // Materials const material = new THREE.MeshStandardMaterial({ color: 'gray', map: textureImg, displacementMap: heightImg, displacementScale: 0.5, depthTest: true }) // Mesh const planeMesh = new THREE.Mesh(geometry, material); scene.add(planeMesh); planeMesh.rotation.x = -Math.PI/2 // Lights const pointLight = new THREE.PointLight('#dcdcff', 2) pointLight.position.x = 3 pointLight.position.y = 3 pointLight.position.z = 3 scene.add(pointLight) // Sizes const sizes = { width: window.innerWidth, height: window.innerHeight } window.addEventListener('resize', () => { // Update sizes sizes.width = window.innerWidth sizes.height = window.innerHeight // Update camera camera.aspect = sizes.width / sizes.height camera.updateProjectionMatrix() // Update renderer renderer.setSize(sizes.width, sizes.height) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) }) // Base camera const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100) camera.position.y = 1 scene.add(camera) // Renderer const renderer = new THREE.WebGLRenderer({ canvas: canvas }) renderer.setSize(sizes.width, sizes.height) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) // Cylinder var gridMin = -1.4 var gridMax = 1.4 var gridDelta = 0.2 // 创建Canvas读取位移贴图像素 const canvasHeight = document.createElement('canvas') const ctxHeight = canvasHeight.getContext('2d') canvasHeight.width = heightImg.image.width canvasHeight.height = heightImg.image.height ctxHeight.drawImage(heightImg.image, 0, 0) const imageData = ctxHeight.getImageData(0, 0, canvasHeight.width, canvasHeight.height) const pixels = imageData.data for (var x=gridMin;x<gridMax;x+=gridDelta) { for (var z=gridMin;z<gridMax;z+=gridDelta) { const cylinderGeometry = new THREE.CylinderGeometry( 0.01, 0.01, 0.05, 20 ); const cylinderMaterial = new THREE.MeshStandardMaterial( {color: 'gray'} ); const cylinderMesh = new THREE.Mesh( cylinderGeometry, cylinderMaterial ); cylinderMesh.position.x = x cylinderMesh.position.z = z // 计算当前位置对应的UV坐标 const u = (x + width/2) / width const v = (z + height/2) / height // 计算像素坐标(Canvas Y轴与Three.js UV Y轴相反) const px = Math.floor(u * canvasHeight.width) const py = canvasHeight.height - Math.floor(v * canvasHeight.height) - 1 // 读取灰度值(位移贴图为灰度图,取R通道即可) const gray = pixels[(py * canvasHeight.width + px) * 4] / 255 // 计算地形高度:灰度值范围[0,1],对应位移范围[-0.5*scale, 0.5*scale] const terrainHeight = (gray - 0.5) * material.displacementScale // 设置圆柱体位置:加上自身高度一半,确保底部贴合地形 cylinderMesh.position.y = terrainHeight + 0.05 / 2 scene.add(cylinderMesh); } } var mwdelta = 0; document.addEventListener( 'mousewheel', (event) => { mwdelta +=event.deltaY; }); // Clock let cameraRadius = 3 const clock = new THREE.Clock() const tick = () => { const elapsedTime = clock.getElapsedTime() var t = 0.125 * elapsedTime var zoomRadius = cameraRadius + mwdelta / 500 camera.position.x = zoomRadius * Math.sin(t) camera.position.z = zoomRadius * Math.cos(t) camera.rotation.y = t // Render renderer.render(scene, camera) // Call tick again on the next frame window.requestAnimationFrame(tick) } tick()
关键说明
- 读取位移贴图像素时,要注意Canvas的Y轴与Three.js的UV Y轴方向相反,需要反转计算
- 灰度值转高度时,要匹配
displacementMap的默认行为:灰度0对应-0.5倍缩放值,灰度1对应0.5倍缩放值 - 圆柱体Y位置需加上自身高度的一半,才能保证底部完全贴合地形表面
内容的提问来源于stack exchange,提问作者ledlogic
相关产品推荐
相关产品推荐

