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

Cannon拖车自主移动异常:3D卡车模拟器技术排查

3D卡车模拟器拖车自主移动异常排查

问题背景

场景细节

  • 地面完全平坦且水平
  • 车轮摩擦参数设置:frictionSlip: 1.4
  • 未对拖车施加任何外力

效果对比

预期效果:拖车保持静止状态
实际情况:拖车沿Z轴自主移动(可通过坐标面板观察位置变化)
关键发现:仅当拖车两端对称布置车轴时,移动现象停止

核心疑问

请问我忽略了什么导致该异常?


最小复现代码

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

const infoBox = document.createElement("div");
infoBox.className =
  "fixed top-4 left-4 bg-black bg-opacity-50 text-white p-4 rounded-lg font-mono z-10";
document.body.appendChild(infoBox);

const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(20, 30, 20);
directionalLight.castShadow = true;
directionalLight.shadow.camera.top = 20;
directionalLight.shadow.camera.bottom = -20;
directionalLight.shadow.camera.left = -20;
directionalLight.shadow.camera.right = 20;
scene.add(directionalLight);

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world);
world.solver.iterations = 10;

const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x4a5d45 });
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
scene.add(groundMesh);

const groundBody = new CANNON.Body({
  mass: 0,
  shape: new CANNON.Box(new CANNON.Vec3(50, 0.1, 50)),
  material: new CANNON.Material("groundMaterial")
});
groundBody.position.y = -0.1;
world.addBody(groundBody);

const cones = [];
const conePositions = [
  { x: -1.75, z: -4 },
  { x: 1.75, z: -4 },
  { x: -1.75, z: 4 },
  { x: 1.75, z: 4 }
];

function createCone(x, z) {
  const coneHeight = 0.8;
  const coneRadius = 0.3;
  const coneGeometry = new THREE.ConeGeometry(coneRadius, coneHeight, 16);
  const coneMaterial = new THREE.MeshStandardMaterial({ color: 0xff4500 });
  const coneMesh = new THREE.Mesh(coneGeometry, coneMaterial);
  coneMesh.position.set(x, coneHeight / 2, z);
  coneMesh.castShadow = true;
  scene.add(coneMesh);
  const coneShape = new CANNON.Cylinder(coneRadius, coneRadius, coneHeight, 16);
  const coneBody = new CANNON.Body({ mass: 1, shape: coneShape });
  coneBody.position.copy(coneMesh.position);
  world.addBody(coneBody);
  cones.push({ mesh: coneMesh, body: coneBody });
}
conePositions.forEach((pos) => createCone(pos.x, pos.z));

/*******************************************************************************
 * Trailer
 *******************************************************************************/
const vehicleLength = 7;
const chassisShape = new CANNON.Box(new CANNON.Vec3(1, 0.5, vehicleLength / 2));
const chassisBody = new CANNON.Body({ mass: 3000 });
chassisBody.addShape(chassisShape);

// These are the legs of the trailer
const legWidth = 0.5;
const legShape = new CANNON.Box(new CANNON.Vec3(1, legWidth, legWidth));
chassisBody.addShape(
  legShape,
  new CANNON.Vec3(0, -legWidth / 2, vehicleLength / 2)
);

chassisBody.position.set(0, 2, 0);
const vehicle = new CANNON.RaycastVehicle({
  chassisBody: chassisBody,
  indexRightAxis: 0,
  indexUpAxis: 1,
  indexForwardAxis: 2
});
vehicle.addToWorld(world);

const chassisGeometry = new THREE.BoxGeometry(2.4, 1, vehicleLength);
const chassisMaterial = new THREE.MeshStandardMaterial({ color: 0xcc0000 });
const chassisMesh = new THREE.Mesh(chassisGeometry, chassisMaterial);
chassisMesh.castShadow = true;
scene.add(chassisMesh);

const radius = 0.52;
const wheelOptions = {
  radius: radius,
  directionLocal: new CANNON.Vec3(0, -1, 0),
  suspensionStiffness: 30,
  suspensionRestLength: 0.3,
  frictionSlip: 1.4,
  dampingRelaxation: 2.3,
  dampingCompression: 4.4,
  maxSuspensionForce: 100000,
  rollInfluence: 0.01,
  axleLocal: new CANNON.Vec3(-1, 0, 0),
  chassisConnectionPointLocal: new CANNON.Vec3(),
  maxSuspensionTravel: 0.3
};
// wheelOptions.chassisConnectionPointLocal.set(1.1, 0, 2.0); vehicle.addWheel(wheelOptions);
// wheelOptions.chassisConnectionPointLocal.set(-1.1, 0, 2.0); vehicle.addWheel(wheelOptions);
wheelOptions.chassisConnectionPointLocal.set(1.1, 0, -2.5);
vehicle.addWheel(wheelOptions);
wheelOptions.chassisConnectionPointLocal.set(-1.1, 0, -2.5);
vehicle.addWheel(wheelOptions);

/************************************************************************************************/
// Comment this part
wheelOptions.chassisConnectionPointLocal.set(1.1, 0, -1.3);
vehicle.addWheel(wheelOptions);
wheelOptions.chassisConnectionPointLocal.set(-1.1, 0, -1.3);
vehicle.addWheel(wheelOptions);
/************************************************************************************************/

const wheelMeshes = [];
vehicle.wheelInfos.forEach(() => {
  const wheelGeometry = new THREE.CylinderGeometry(radius, radius, 0.4, 32);
  const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x1a1a1a });
  const wheelMesh = new THREE.Mesh(wheelGeometry, wheelMaterial);
  const wheel = new THREE.Group();
  wheel.add(wheelMesh);
  wheelMesh.rotateZ(Math.PI / 2);
  scene.add(wheel);
  wheelMeshes.push(wheel);
});

let isMouseDown = false;
let lastMouseX = null;
let lastMouseY = null;
let cameraTheta = Math.PI / 2;
let cameraPhi = Math.PI / 3;
const cameraRadius = 18;

document.addEventListener("mousedown", (event) => {
  isMouseDown = true;
  lastMouseX = event.clientX;
  lastMouseY = event.clientY;
});
document.addEventListener("mouseup", () => (isMouseDown = false));
document.addEventListener("mousemove", (event) => {
  if (!isMouseDown) return;
  const deltaX = event.clientX - lastMouseX;
  const deltaY = event.clientY - lastMouseY;
  lastMouseX = event.clientX;
  lastMouseY = event.clientY;
  cameraTheta -= deltaX * 0.005;
  cameraPhi -= deltaY * 0.005;
  cameraPhi = Math.max(0.1, Math.min(Math.PI / 2.2, cameraPhi));
});

const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);

  const deltaTime = clock.getDelta();
  world.step(1 / 60, deltaTime);

  chassisMesh.position.copy(chassisBody.position);
  chassisMesh.quaternion.copy(chassisBody.quaternion);
  for (let i = 0; i < vehicle.wheelInfos.length; i++) {
    vehicle.updateWheelTransform(i);
    wheelMeshes[i].position.copy(vehicle.wheelInfos[i].worldTransform.position);
    wheelMeshes[i].quaternion.copy(
      vehicle.wheelInfos[i].worldTransform.quaternion
    );
  }

  const truckPosition = chassisMesh.position;
  const offsetX = cameraRadius * Math.sin(cameraPhi) * Math.sin(cameraTheta);
  const offsetY = cameraRadius * Math.cos(cameraPhi);
  const offsetZ = cameraRadius * Math.sin(cameraPhi) * Math.cos(cameraTheta);
  const cameraTargetPosition = new THREE.Vector3(
    truckPosition.x + offsetX,
    truckPosition.y + offsetY,
    truckPosition.z + offsetZ
  );
  camera.position.lerp(cameraTargetPosition, 0.15);
  camera.lookAt(truckPosition);

  // Update info box
  const pos = chassisMesh.position;
  const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(
    chassisMesh.quaternion
  );
  let angle = Math.atan2(forward.z, forward.x) * (180 / Math.PI);

  const positionText = `Position:<br>X:${pos.x.toFixed(2)}<br>Y:${pos.y.toFixed(
    2
  )}<br>Z:${pos.z.toFixed(2)}`;
  const angleText = `Angle (X): ${angle.toFixed(2)}°`;
  infoBox.innerHTML = `${positionText}<br>${angleText}`;

  renderer.render(scene, camera);
}

window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();

问题分析与解决方案

核心原因

拖车自主移动的本质是非对称车轴布置引发的悬挂力矩失衡,叠加物理引擎的数值求解误差积累:

  1. 悬挂力矩失衡:当前车轴全部集中在拖车后半段(Z=-2.5、Z=-1.3),而拖车重心因底盘和附加腿结构位于更靠前的位置。每个车轮的悬挂力用于平衡重力,但由于力的作用点不在重心正下方,会对底盘产生微小的旋转力矩,这些力矩无法通过对称反作用力抵消,导致底盘出现旋转趋势。
  2. 数值误差积累:Cannon.js的接触求解器无法做到绝对精确,微小的旋转趋势会让车轮与地面产生极细微的相对滑动。摩擦力试图阻止滑动,但非对称受力分布会产生沿Z轴的净力,随着时间推移积累为明显的自主移动。

当车轴对称布置时,前后悬挂的力矩相互抵消,数值误差也被对称结构平均,因此拖车保持静止。

修复方案

方案1:对称布置车轴

取消注释前端的两个车轴代码,让前后车轴对称分布,直接抵消悬挂力矩:

wheelOptions.chassisConnectionPointLocal.set(1.1, 0, 2.0); vehicle.addWheel(wheelOptions);
wheelOptions.chassisConnectionPointLocal.set(-1.1, 0, 2.0); vehicle.addWheel(wheelOptions);

方案2:匹配重心与车轴位置

如果不需要前端车轴,重新计算拖车重心,将所有车轴布置在重心正下方(或对称分布在重心两侧),确保悬挂力的合力通过重心,避免产生旋转力矩。可通过调整chassisBody的形状位置或添加额外质量分布修正重心。


内容的提问来源于stack exchange,提问作者Rickson Guidolini

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 15:45:54