You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 10:10:13