32 · 车辆沿斜坡移动、物体贴墙滑行
需求与速度分解
角色输入通常给出水平速度,但地面可能倾斜。直接改变 x、z 而不调整 y 会钻入斜坡。给定单位表面法线 n,把期望速度 v 分为法向与切向:
projectOnPlane(n) 计算切向分量。若 n 没有归一化,投影公式需要除以 n·n;Three.js 的 projectOnPlane 内部借助向量投影处理这个分母,但业务代码仍应尽量使用单位法线,便于距离、碰撞和反射统一计算。
投影会降低速度大小。沿坡面始终保持相同速度,可投影后重新归一化再乘速度;这属于运动设计选择,不等同于物理受力。墙面滑行一般保留切向分量,不重新放大速度,否则斜撞墙时会出现不自然加速。
完整核心代码
import { createDemo } from './runtime.js';
const { THREE, scene, onFrame } = createDemo();
const normal = new THREE.Vector3(0, 1, -0.5).normalize();
// 平面方程 y - 0.5z = 0,法线朝上。
const plane = new THREE.Plane(normal, 0);
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshNormalMaterial({ side: THREE.DoubleSide })
);
ground.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), normal);
scene.add(ground);
const radius = 0.25;
const actor = new THREE.Mesh(new THREE.SphereGeometry(radius), new THREE.MeshNormalMaterial());
actor.position.copy(normal).multiplyScalar(radius);
scene.add(actor);
const desiredVelocity = new THREE.Vector3(0, 0, 2);
const tangentVelocity = new THREE.Vector3();
const surfacePoint = new THREE.Vector3();
onFrame((dt, elapsed) => {
// 来回沿坡面行进,保证始终留在示例有限平面内。
desiredVelocity.z = Math.sin(elapsed * 0.8) >= 0 ? 2 : -2;
tangentVelocity.copy(desiredVelocity).projectOnPlane(normal);
if (tangentVelocity.lengthSq() > 1e-10) tangentVelocity.setLength(2);
actor.position.addScaledVector(tangentVelocity, dt);
// 消除数值漂移:球心与平面保持 radius 的法向距离。
plane.projectPoint(actor.position, surfacePoint);
actor.position.copy(surfacePoint).addScaledVector(normal, radius);
});
// 墙法线必须是指向自由空间的单位向量。
function removeVelocityIntoWall(velocity, wallNormal) {
const inwardSpeed = velocity.dot(wallNormal);
if (inwardSpeed < 0) velocity.addScaledVector(wallNormal, -inwardSpeed);
return velocity;
}
墙面函数只移除朝墙内运动的分量。无条件投影会连离开墙面的速度也移除,使角色黏在墙上。地面函数则假定角色一直处于接触状态,所以每帧约束到坡面。
验证与限制
检查球心到平面的距离是否始终约等于半径;将坡度调大后观察投影前后的速度差异。当期望方向几乎与法线平行时,投影长度趋近零,不能盲目归一化。
本例不是完整碰撞系统:没有跳跃、失去接触、有限坡面边缘、台阶或多个接触面。真实角色控制器通常先求碰撞、更新接触状态,再约束速度。把射线命中面的局部法线当成世界法线是常见错误;需要按对象的法线矩阵转换,并归一化。车辆还涉及轮胎接触和悬挂,应交给物理系统或专用模型。
练习(暂不提供答案)
- 去掉
setLength(2),测量沿坡速度随坡度如何变化。 - 加入最大可行走坡角,通过法线与世界上方向的点积判断。
- 验证墙面函数保留离墙速度,却去除入墙速度。
- 增加重力与接触标志,让球可以离开有限坡面。