현실적인 차량 움직임을 위해서는 서스펜션 시스템이 필수적입니다.
Rapier의 스프링 조인트를 활용하여 구현해보겠습니다.
interface SuspensionConfig {
springStiffness: number; // 스프링 강도
springDamping: number; // 댐핑 계수
restLength: number; // 기본 길이
maxTravel: number; // 최대 이동 거리
}
class Suspension {
private joint: RAPIER.SpringJoint;
private config: SuspensionConfig;
constructor(world: RAPIER.World, wheelBody: RAPIER.RigidBody, chassisBody: RAPIER.RigidBody, mountPoint: RAPIER.Vector3, config: SuspensionConfig) {
this.config = config;
// 스프링 조인트 생성
const params = RAPIER.JointData.spring(
mountPoint, // 차체 연결점
new RAPIER.Vector3(0, -config.restLength, 0), // 휠 연결점
config.springStiffness, // 스프링 강도
config.springDamping // 댐핑
);
this.joint = world.createImpulseJoint(params, chassisBody, wheelBody, true);
// 최대 이동 거리 제한 설정
this.joint.setLimits(-config.maxTravel, config.maxTravel);
}
getCurrentCompression(): number {
return this.joint.getCurrentDistance();
}
// 서스펜션 힘 조정 (노면 상태나 주행 모드에 따라)
adjustSpringForce(stiffnessMultiplier: number) {
const newStiffness = this.config.springStiffness * stiffnessMultiplier;
this.joint.setSpringStiffness(newStiffness);
}
}
실제 차량의 동력 전달 과정을 시뮬레이션합니다.
interface EngineConfig {
maxTorque: number; // 최대 토크
maxRPM: number; // 최대 RPM
gearRatios: number[]; // 기어비
finalDriveRatio: number; // 최종 감속비
}
class Engine {
private config: EngineConfig;
private currentRPM: number = 0;
private currentGear: number = 1;
constructor(config: EngineConfig) {
this.config = config;
}
calculateTorque(throttle: number): number {
// 토크 곡선 시뮬레이션
const normalizedRPM = this.currentRPM / this.config.maxRPM;
const torqueCurve = 4 * normalizedRPM * (1 - normalizedRPM); // 간단한 토크 곡선
return this.config.maxTorque * torqueCurve * throttle;
}
applyPowerToWheels(wheels: RAPIER.RigidBody[], throttle: number) {
const torque = this.calculateTorque(throttle);
const gearRatio = this.config.gearRatios[this.currentGear - 1];
const finalTorque = torque * gearRatio * this.config.finalDriveRatio;
wheels.forEach(wheel => {
wheel.applyTorqueImpulse(new RAPIER.Vector3(finalTorque, 0, 0));
});
// RPM 업데이트
this.currentRPM = this.calculateEngineRPM(wheels);
}
private calculateEngineRPM(wheels: RAPIER.RigidBody[]): number {
// 휠 회전 속도로부터 엔진 RPM 계산
const wheelRPM = wheels.reduce((sum, wheel) => {
return sum + Math.abs(wheel.angularVelocity().x);
}, 0) / wheels.length;
return wheelRPM * this.config.gearRatios[this.currentGear - 1] *
this.config.finalDriveRatio * 60 / (2 * Math.PI);
}
}
차량의 공기역학적 특성을 시뮬레이션합니다.
interface AerodynamicsConfig {
dragCoefficient: number; // 항력 계수
frontalArea: number; // 전면적
liftCoefficient: number; // 양력 계수
airDensity: number; // 공기 밀도
}
class Aerodynamics {
private config: AerodynamicsConfig;
constructor(config: AerodynamicsConfig) {
this.config = config;
}
applyAerodynamicForces(carBody: RAPIER.RigidBody, velocity: RAPIER.Vector3) {
const speed = velocity.length();
const direction = velocity.normalize();
// 항력 계산
const dragMagnitude = 0.5 * this.config.airDensity * speed * speed *
this.config.dragCoefficient * this.config.frontalArea;
// 양력 계산
const liftMagnitude = 0.5 * this.config.airDensity * speed * speed *
this.config.liftCoefficient * this.config.frontalArea;
// 힘 적용
carBody.applyForce(
new RAPIER.Vector3(
-dragMagnitude * direction.x,
-liftMagnitude,
-dragMagnitude * direction.z
)
);
}
}
실제 타이어의 특성을 시뮬레이션하는 Pacejka's Magic Formula를 구현합니다.
interface TireConfig {
B: number; // 강성 계수
C: number; // 형상 계수
D: number; // 최대값
E: number; // 곡률 계수
}
class TireModel {
private config: TireConfig;
constructor(config: TireConfig) {
this.config = config;
}
// Pacejka's Magic Formula
calculateLateralForce(slipAngle: number, normalForce: number): number {
const { B, C, D, E } = this.config;
const x = Math.atan(slipAngle);
return D * Math.sin(C * Math.atan(B * x - E * (B * x - Math.atan(B * x))));
}
calculateLongitudinalForce(slipRatio: number, normalForce: number): number {
const { B, C, D, E } = this.config;
return D * Math.sin(C * Math.atan(B * slipRatio - E * (B * slipRatio - Math.atan(B * slipRatio))));
}
}
충돌에 따른 차량 손상을 시뮬레이션합니다.
interface DamageConfig {
maxDeformation: number; // 최대 변형량
strengthMultiplier: number; // 강도 계수
deformationThreshold: number;// 변형 시작 임계값
}
class DamageSystem {
private config: DamageConfig;
private deformationMap: Map<string, THREE.Vector3> = new Map();
constructor(config: DamageConfig) {
this.config = config;
}
processCollision(
mesh: THREE.Mesh,
collisionPoint: THREE.Vector3,
collisionForce: number
) {
if (collisionForce < this.config.deformationThreshold) {
return;
}
const geometry = mesh.geometry;
const positionAttribute = geometry.getAttribute('position');
const positions = positionAttribute.array;
// 충돌 지점 주변의 버텍스들을 변형
for (let i = 0; i < positions.length; i += 3) {
const vertexPosition = new THREE.Vector3(
positions[i],
positions[i + 1],
positions[i + 2]
);
const distance = vertexPosition.distanceTo(collisionPoint);
if (distance < this.config.maxDeformation) {
const deformationVector = vertexPosition
.sub(collisionPoint)
.normalize()
.multiplyScalar(
collisionForce *
(1 - distance / this.config.maxDeformation) *
this.config.strengthMultiplier
);
positions[i] += deformationVector.x;
positions[i + 1] += deformationVector.y;
positions[i + 2] += deformationVector.z;
this.deformationMap.set(
`${i}`,
new THREE.Vector3(
deformationVector.x,
deformationVector.y,
deformationVector.z
)
);
}
}
positionAttribute.needsUpdate = true;
geometry.computeVertexNormals();
}
}
위의 모든 시스템을 통합하여 사용하는 예시입니다.
class AdvancedCar extends Car {
private suspension: Suspension[];
private engine: Engine;
private aerodynamics: Aerodynamics;
private tireModel: TireModel;
private damageSystem: DamageSystem;
constructor() {
super();
this.engine = new Engine({
maxTorque: 500,
maxRPM: 8000,
gearRatios: [3.4, 2.0, 1.3, 1.0, 0.7],
finalDriveRatio: 3.7
});
this.aerodynamics = new Aerodynamics({
dragCoefficient: 0.3,
frontalArea: 2.2,
liftCoefficient: 0.2,
airDensity: 1.225
});
this.tireModel = new TireModel({
B: 10,
C: 1.9,
D: 1,
E: 0.97
});
this.damageSystem = new DamageSystem({
maxDeformation: 0.5,
strengthMultiplier: 0.1,
deformationThreshold: 1000
});
}
override update() {
super.update();
// 엔진 파워 적용
const wheels = this.dynamicBodies.slice(1).map(([, body]) => body);
this.engine.applyPowerToWheels(wheels, this.throttle);
// 공기역학 힘 적용
const carBody = this.dynamicBodies[0][1];
this.aerodynamics.applyAerodynamicForces(
carBody,
carBody.linvel()
);
// 타이어 힘 계산 및 적용
wheels.forEach((wheel, index) => {
const slipAngle = this.calculateSlipAngle(wheel);
const normalForce = this.calculateNormalForce(wheel);
const lateralForce = this.tireModel.calculateLateralForce(
slipAngle,
normalForce
);
wheel.applyForce(new RAPIER.Vector3(0, 0, lateralForce));
});
}
handleCollision(collisionEvent: RAPIER.CollisionEvent) {
const collisionPoint = collisionEvent.contactPoint;
const collisionForce = collisionEvent.impulse.length();
this.damageSystem.processCollision(
this.dynamicBodies[0][0] as THREE.Mesh,
new THREE.Vector3(
collisionPoint.x,
collisionPoint.y,
collisionPoint.z
),
collisionForce
);
}
private calculateSlipAngle(wheel: RAPIER.RigidBody): number {
// 휠의 방향과 실제 이동 방향 사이의 각도 계산
const wheelDirection = new THREE.Vector3(0, 0, 1)
.applyQuaternion(wheel.rotation());
const velocityDirection = new THREE.Vector3()
.fromArray(wheel.linvel().toArray())
.normalize();
return Math.acos(wheelDirection.dot(velocityDirection));
}
private calculateNormalForce(wheel: RAPIER.RigidBody): number {
// 휠에 작용하는 수직항력 계산
return wheel.mass() * 9.81 / 4; // 간단한 근사값
}
}
위의 모든 시스템을 통합하여 사용하는 예시입니다.
class AdvancedCar extends Car {
private suspension: Suspension[];
private engine: Engine;
private aerodynamics: Aerodynamics;
private tireModel: TireModel;
private damageSystem: DamageSystem;
constructor() {
super();
this.engine = new Engine({
maxTorque: 500,
maxRPM: 8000,
gearRatios: [3.4, 2.0, 1.3, 1.0, 0.7],
finalDriveRatio: 3.7
});
this.aerodynamics = new Aerodynamics({
dragCoefficient: 0.3,
frontalArea: 2.2,
liftCoefficient: 0.2,
airDensity: 1.225
});
this.tireModel = new TireModel({
B: 10,
C: 1.9,
D: 1,
E: 0.97
});
this.damageSystem = new DamageSystem({
maxDeformation: 0.5,
strengthMultiplier: 0.1,
deformationThreshold: 1000
});
}
override update() {
super.update();
// 엔진 파워 적용
const wheels = this.dynamicBodies.slice(1).map(([, body]) => body);
this.engine.applyPowerToWheels(wheels, this.throttle);
// 공기역학 힘 적용
const carBody = this.dynamicBodies[0][1];
this.aerodynamics.applyAerodynamicForces(
carBody,
carBody.linvel()
);
// 타이어 힘 계산 및 적용
wheels.forEach((wheel, index) => {
const slipAngle = this.calculateSlipAngle(wheel);
const normalForce = this.calculateNormalForce(wheel);
const lateralForce = this.tireModel.calculateLateralForce(
slipAngle,
normalForce
);
wheel.applyForce(new RAPIER.Vector3(0, 0, lateralForce));
});
}
handleCollision(collisionEvent: RAPIER.CollisionEvent) {
const collisionPoint = collisionEvent.contactPoint;
const collisionForce = collisionEvent.impulse.length();
this.damageSystem.processCollision(
this.dynamicBodies[0][0] as THREE.Mesh,
new THREE.Vector3(
collisionPoint.x,
collisionPoint.y,
collisionPoint.z
),
collisionForce
);
}
private calculateSlipAngle(wheel: RAPIER.RigidBody): number {
// 휠의 방향과 실제 이동 방향 사이의 각도 계산
const wheelDirection = new THREE.Vector3(0, 0, 1)
.applyQuaternion(wheel.rotation());
const velocityDirection = new THREE.Vector3()
.fromArray(wheel.linvel().toArray())
.normalize();
return Math.acos(wheelDirection.dot(velocityDirection));
}
private calculateNormalForce(wheel: RAPIER.RigidBody): number {
// 휠에 작용하는 수직항력 계산
return wheel.mass() * 9.81 / 4; // 간단한 근사값
}
}
class PhysicsWorker {
private worker: Worker;
private stepSize: number = 1 / 60;
private accumulator: number = 0;
constructor() {
this.worker = new Worker('physics-worker.js');
this.worker.onmessage = (e) => {
// 물리 연산 결과 처리
this.updateBodies(e.data);
};
}
step(deltaTime: number) {
this.accumulator += deltaTime;
while (this.accumulator >= this.stepSize) {
this.worker.postMessage({
type: 'step',
stepSize: this.stepSize
});
this.accumulator -= this.stepSize;
}
}
원거리 차량의 물리 연산을 최적화합니다.
enum PhysicsLOD {
FULL, // 모든 물리 시뮬레이션
SIMPLIFIED, // 단순화된 물리
KINEMATIC // 물리 없이 움직임만
}
class PhysicsLODSystem {
private lodThresholds = {
FULL: 20, // 20미터 이내
SIMPLIFIED: 50 // 50미터 이내
};
calculateLODLevel(distance: number): PhysicsLOD {
if (distance <= this.lodThresholds.FULL) {
return PhysicsLOD.FULL;
} else if (distance <= this.lodThresholds.SIMPLIFIED) {
return PhysicsLOD.SIMPLIFIED;
}
return PhysicsLOD.KINEMATIC;
}
updatePhysics(car: AdvancedCar, camera: THREE.Camera) {
const distance = car.position.distanceTo(camera.position);
const lodLevel = this.calculateLODLevel(distance);
switch (lodLevel) {
case PhysicsLOD.FULL:
car.updateFullPhysics();
break;
case PhysicsLOD.SIMPLIFIED:
car.updateSimplifiedPhysics();
break;
case PhysicsLOD.KINEMATIC:
car.updateKinematic();
break;
}
}
}
class VehicleInstanceManager {
private instancedMesh: THREE.InstancedMesh;
private vehicles: AdvancedCar[] = [];
private maxInstances: number = 1000;
constructor(baseMesh: THREE.Mesh) {
const geometry = baseMesh.geometry;
const material = baseMesh.material;
this.instancedMesh = new THREE.InstancedMesh(
geometry,
material,
this.maxInstances
);
}
addVehicle(vehicle: AdvancedCar, position: THREE.Vector3) {
const index = this.vehicles.length;
if (index >= this.maxInstances) return;
this.vehicles.push(vehicle);
const matrix = new THREE.Matrix4();
matrix.setPosition(position);
this.instancedMesh.setMatrixAt(index, matrix);
this.instancedMesh.instanceMatrix.needsUpdate = true;
}
update() {
this.vehicles.forEach((vehicle, index) => {
const matrix = new THREE.Matrix4();
matrix.compose(
vehicle.position,
vehicle.quaternion,
vehicle.scale
);
this.instancedMesh.setMatrixAt(index, matrix);
});
this.instancedMesh.instanceMatrix.needsUpdate = true;
}
}
interface WeatherConfig {
rainIntensity: number; // 0-1 사이의 강우 강도
waterLevel: number; // 도로 위 물 깊이
windDirection: THREE.Vector3; // 바람 방향
windStrength: number; // 바람 세기
}
class WeatherSystem {
private config: WeatherConfig;
private roadCondition: Map<string, number> = new Map(); // 도로 구획별 마찰 계수
constructor(config: WeatherConfig) {
this.config = config;
}
updateVehicleDynamics(car: AdvancedCar) {
// 비에 의한 타이어 마찰력 감소
const frictionModifier = 1 - (this.config.rainIntensity * 0.5);
car.setTireFriction(frictionModifier);
// 수막현상 시뮬레이션
if (this.config.waterLevel > 0.05) {
const speed = car.getSpeed();
const hydroplaningRisk =
(speed * this.config.waterLevel) / car.getTireGrooveDepth();
if (hydroplaningRisk > 1) {
car.reduceGrip(hydroplaningRisk);
}
}
// 바람 영향
const windForce = this.config.windDirection.clone()
.multiplyScalar(this.config.windStrength);
car.applyAerodynamicForce(windForce);
}
}
enum SurfaceType {
ASPHALT,
DIRT,
GRASS,
GRAVEL,
ICE,
SNOW
}
interface SurfaceProperties {
friction: number;
roughness: number;
deformation: number;
}
class RoadSurfaceSystem {
private surfaceMap: Map<SurfaceType, SurfaceProperties> = new Map([
[SurfaceType.ASPHALT, { friction: 1.0, roughness: 0.1, deformation: 0.0 }],
[SurfaceType.DIRT, { friction: 0.7, roughness: 0.5, deformation: 0.2 }],
[SurfaceType.GRASS, { friction: 0.5, roughness: 0.3, deformation: 0.3 }],
[SurfaceType.GRAVEL, { friction: 0.6, roughness: 0.7, deformation: 0.1 }],
[SurfaceType.ICE, { friction: 0.2, roughness: 0.05, deformation: 0.0 }],
[SurfaceType.SNOW, { friction: 0.3, roughness: 0.2, deformation: 0.4 }]
]);
updateVehicleBehavior(car: AdvancedCar, surfaceType: SurfaceType) {
const surface = this.surfaceMap.get(surfaceType)!;
// 노면 특성에 따른 차량 거동 조정
car.setTireFriction(surface.friction);
car.setSuspensionDamping(1 / surface.roughness);
// 노면 변형 시뮬레이션
if (surface.deformation > 0) {
this.simulateGroundDeformation(car, surface.deformation);
}
}
private simulateGroundDeformation(car: AdvancedCar, deformationFactor: number) {
const wheelPositions = car.getWheelPositions();
wheelPositions.forEach(pos => {
// 지면 메시 변형 처리
this.deformGroundMesh(pos, car.getWheelLoad() * deformationFactor);
// 변형된 지면에 의한 차량 높이 조정
car.adjustWheelHeight(this.getGroundHeight(pos));
});
}
}
class TrafficAI {
private vehicles: AdvancedCar[] = [];
private pathfinder: Pathfinder;
private trafficRules: TrafficRuleEngine;
constructor() {
this.pathfinder = new Pathfinder();
this.trafficRules = new TrafficRuleEngine();
}
update(deltaTime: number) {
this.vehicles.forEach(vehicle => {
// 경로 계획
const currentPath = this.pathfinder.getPath(
vehicle.position,
vehicle.destination
);
// 장애물 감지 및 회피
const obstacles = this.detectObstacles(vehicle);
const avoidanceForce = this.calculateAvoidanceForce(vehicle, obstacles);
// 교통 규칙 준수
const trafficSignals = this.trafficRules.getRelevantSignals(vehicle);
const shouldStop = this.trafficRules.shouldStop(vehicle, trafficSignals);
if (shouldStop) {
vehicle.brake();
} else {
// AI 주행 로직
const steeringForce = this.calculateSteeringForce(
vehicle,
currentPath,
avoidanceForce
);
vehicle.steer(steeringForce);
const throttle = this.calculateThrottle(
vehicle,
currentPath,
obstacles
);
vehicle.setThrottle(throttle);
}
});
}
private calculateSteeringForce(
vehicle: AdvancedCar,
path: Path,
avoidanceForce: THREE.Vector3
): number {
const ahead = vehicle.position.clone().add(
vehicle.forward.multiplyScalar(10)
);
const target = path.getNextWaypoint(ahead);
const desiredDirection = target.clone()
.sub(vehicle.position)
.normalize();
const currentDirection = vehicle.forward;
// 회피력을 고려한 조향각 계산
let steeringAngle = Math.atan2(
desiredDirection.z + avoidanceForce.z,
desiredDirection.x + avoidanceForce.x
) - Math.atan2(currentDirection.z, currentDirection.x);
// 조향각 제한
return Math.max(-0.5, Math.min(0.5, steeringAngle));
}
}
interface VehicleState {
position: THREE.Vector3;
rotation: THREE.Quaternion;
velocity: THREE.Vector3;
steering: number;
throttle: number;
brake: number;
}
class NetworkSync {
private static readonly SYNC_INTERVAL = 50; // 50ms
private static readonly INTERPOLATION_DELAY = 100; // 100ms
private lastSyncTime: number = 0;
private states: VehicleState[] = [];
syncVehicle(vehicle: AdvancedCar, currentTime: number) {
if (currentTime - this.lastSyncTime >= NetworkSync.SYNC_INTERVAL) {
this.sendVehicleState(vehicle);
this.lastSyncTime = currentTime;
}
this.interpolateVehicleState(vehicle, currentTime);
}
private sendVehicleState(vehicle: AdvancedCar) {
const state: VehicleState = {
position: vehicle.position.clone(),
rotation: vehicle.quaternion.clone(),
velocity: vehicle.getVelocity(),
steering: vehicle.steering,
throttle: vehicle.throttle,
brake: vehicle.brake
};
// 네트워크로 상태 전송
this.sendToServer(state);
}
private interpolateVehicleState(vehicle: AdvancedCar, currentTime: number) {
if (this.states.length < 2) return;
const interpolationTime = currentTime - NetworkSync.INTERPOLATION_DELAY;
let i = 0;
// 보간할 두 상태 찾기
while (i < this.states.length - 1 &&
this.states[i + 1].timestamp <= interpolationTime) {
i++;
}
if (i >= this.states.length - 1) return;
const before = this.states[i];
const after = this.states[i + 1];
const alpha = (interpolationTime - before.timestamp) /
(after.timestamp - before.timestamp);
// 상태 보간
vehicle.position.lerpVectors(before.position, after.position, alpha);
vehicle.quaternion.slerp(after.rotation, alpha);
vehicle.setVelocity(vehicle.getVelocity().lerp(after.velocity, alpha));
}
}
이 시스템들을 통합하여 사용하면 실제 차량의 물리적 특성을 매우 사실적으로 시뮬레이션할 수 있습니다.
특히 다음과 같은 상황에서 뛰어난 성능을 보입니다.
1. 험로 주행 시 서스펜션의 실제적인 거동
2. 고속 주행 시 공기역학적 영향
3. 날씨 변화에 따른 노면 마찰력 변화
4. 다중 차량 시뮬레이션에서의 성능 최적화
5. 네트워크 멀티플레이어 환경에서의 부드러운 동기화
이러한 시스템은 게임 엔진이나 시뮬레이션 소프트웨어에서 실제로 사용되는 방식과 유사하며, 필요에 따라 더 발전시킬 수 있습니다.