Rapier를 활용한 서스펜션, 손상 시스템, 날씨 효과 및 성능 최적화

1. 지형에 따른 서스펜션 시스템 구현

1.1 레이캐스트를 활용한 지면 감지

class AdvancedSuspension {
    private readonly MAX_SUSPENSION_TRAVEL = 0.2; // 서스펜션 최대 이동 거리 (미터)
    private readonly SPRING_STIFFNESS = 50000; // 스프링 강성
    private readonly DAMPING = 4500; // 댐핑 계수
    
    constructor(private wheel: RigidBody, private chassisConnectionPoint: Vector3) {}

    update() {
        const rayStart = this.wheel.translation();
        const rayDir = new Vector3(0, -1, 0);
        const rayLength = this.MAX_SUSPENSION_TRAVEL * 2;

        // 지면 감지를 위한 레이캐스트
        const hit = world.castRay(
            new Ray(rayStart, rayDir),
            rayLength,
            true,
            undefined,
            undefined,
            this.wheel
        );

        if (hit) {
            const compressionLength = rayLength - hit.toi;
            this.applySuspensionForce(compressionLength);
        }
    }

    private applySuspensionForce(compression: number) {
        const springForce = compression * this.SPRING_STIFFNESS;
        const velocity = this.wheel.linvel();
        const dampingForce = velocity.y * this.DAMPING;
        
        const totalForce = springForce + dampingForce;
        this.wheel.applyImpulse(new Vector3(0, totalForce, 0), true);
    }
}

1.2 지형별 서스펜션 동작 최적화

enum TerrainType {
    ASPHALT,
    DIRT,
    SAND,
    SNOW,
    MUD
}

class TerrainAwareSuspension extends AdvancedSuspension {
    private terrainMultipliers = new Map<TerrainType, number>([
        [TerrainType.ASPHALT, 1.0],
        [TerrainType.DIRT, 0.8],
        [TerrainType.SAND, 0.6],
        [TerrainType.SNOW, 0.4],
        [TerrainType.MUD, 0.5]
    ]);

    protected override applySuspensionForce(compression: number) {
        const terrainType = this.detectTerrainType();
        const multiplier = this.terrainMultipliers.get(terrainType) || 1.0;
        
        const adjustedSpringForce = compression * this.SPRING_STIFFNESS * multiplier;
        const adjustedDampingForce = this.wheel.linvel().y * this.DAMPING * multiplier;
        
        const totalForce = adjustedSpringForce + adjustedDampingForce;
        this.wheel.applyImpulse(new Vector3(0, totalForce, 0), true);
    }

    private detectTerrainType(): TerrainType {
        // 레이캐스트 결과로부터 지형 타입 감지
        const hit = this.castTerrainRay();
        if (hit) {
            return this.analyzeTerrainMaterial(hit.collider);
        }
        return TerrainType.ASPHALT;
    }
}

2. 차량 손상 시스템 구현

2.1 충돌 감지 및 손상 계산

interface DamageZone {
    position: Vector3;
    radius: number;
    maxHealth: number;
    currentHealth: number;
}

class VehicleDamageSystem {
    private damageZones: DamageZone[] = [];
    private readonly DAMAGE_THRESHOLD = 50; // 최소 충격량
    
    constructor() {
        this.initializeDamageZones();
    }

    private initializeDamageZones() {
        // 차량의 주요 부위별 손상 영역 정의
        this.damageZones = [
            { position: new Vector3(0, 0, 2), radius: 1, maxHealth: 100, currentHealth: 100 }, // 전면부
            { position: new Vector3(0, 0, -2), radius: 1, maxHealth: 100, currentHealth: 100 }, // 후면부
            { position: new Vector3(1, 0, 0), radius: 0.8, maxHealth: 80, currentHealth: 80 }, // 우측면
            { position: new Vector3(-1, 0, 0), radius: 0.8, maxHealth: 80, currentHealth: 80 }, // 좌측면
        ];
    }

    handleCollision(impact: Vector3, force: number) {
        if (force < this.DAMAGE_THRESHOLD) return;

        this.damageZones.forEach(zone => {
            const distance = impact.distanceTo(zone.position);
            if (distance <= zone.radius) {
                const damage = this.calculateDamage(force, distance, zone.radius);
                this.applyDamage(zone, damage);
            }
        });
    }

    private calculateDamage(force: number, distance: number, radius: number): number {
        const distanceFactor = 1 - (distance / radius);
        return force * distanceFactor;
    }

    private applyDamage(zone: DamageZone, damage: number) {
        zone.currentHealth = Math.max(0, zone.currentHealth - damage);
        this.updateVehiclePerformance();
    }

    private updateVehiclePerformance() {
        // 손상도에 따른 차량 성능 저하 적용
        const averageHealth = this.calculateAverageHealth();
        this.adjustVehicleParameters(averageHealth);
    }
}

2.2 시각적 손상 표현

class VehicleDeformationSystem {
    private originalVertices: Float32Array;
    private deformedVertices: Float32Array;
    private readonly MAX_DEFORMATION = 0.3;

    constructor(private mesh: Mesh) {
        this.initializeVertices();
    }

    private initializeVertices() {
        const positions = this.mesh.geometry.getAttribute('position');
        this.originalVertices = new Float32Array(positions.array);
        this.deformedVertices = new Float32Array(positions.array);
    }

    applyDeformation(impactPoint: Vector3, force: number) {
        const geometry = this.mesh.geometry;
        const positionAttribute = geometry.getAttribute('position');

        for (let i = 0; i < positionAttribute.count; i++) {
            const vertex = new Vector3().fromBufferAttribute(positionAttribute, i);
            const distance = impactPoint.distanceTo(vertex);
            const deformation = this.calculateDeformation(force, distance);
            
            const deformationVector = vertex.sub(impactPoint).normalize().multiplyScalar(deformation);
            
            this.deformedVertices[i * 3] = this.originalVertices[i * 3] + deformationVector.x;
            this.deformedVertices[i * 3 + 1] = this.originalVertices[i * 3 + 1] + deformationVector.y;
            this.deformedVertices[i * 3 + 2] = this.originalVertices[i * 3 + 2] + deformationVector.z;
        }

        positionAttribute.needsUpdate = true;
        geometry.computeVertexNormals();
    }

    private calculateDeformation(force: number, distance: number): number {
        const deformation = (force / (1 + distance)) * this.MAX_DEFORMATION;
        return Math.min(deformation, this.MAX_DEFORMATION);
    }
}

3. 날씨 효과에 따른 물리 변화

enum WeatherCondition {
    CLEAR,
    RAIN,
    SNOW,
    ICE
}

class WeatherPhysicsSystem {
    private currentWeather: WeatherCondition = WeatherCondition.CLEAR;
    private frictionMultipliers = new Map<WeatherCondition, number>([
        [WeatherCondition.CLEAR, 1.0],
        [WeatherCondition.RAIN, 0.7],
        [WeatherCondition.SNOW, 0.4],
        [WeatherCondition.ICE, 0.2]
    ]);

    setWeatherCondition(weather: WeatherCondition) {
        this.currentWeather = weather;
        this.updateVehiclePhysics();
    }

    private updateVehiclePhysics() {
        const multiplier = this.frictionMultipliers.get(this.currentWeather) || 1.0;
        
        // 타이어 마찰력 조정
        this.updateTireFriction(multiplier);
        
        // 브레이크 효과 조정
        this.updateBrakeEffectiveness(multiplier);
        
        // 조향 반응성 조정
        this.updateSteeringResponse(multiplier);
    }

    private updateTireFriction(multiplier: number) {
        const wheels = [this.wheelFLCollider, this.wheelFRCollider, 
                       this.wheelBLCollider, this.wheelBRCollider];
        
        wheels.forEach(wheel => {
            wheel.setFriction(this.BASE_FRICTION * multiplier);
        });
    }
}

4. 성능 최적화 기법

4.1 LOD (Level of Detail) 시스템

class VehicleLODSystem {
    private readonly LOD_DISTANCES = [10, 30, 50]; // 미터 단위
    private readonly PHYSICS_UPDATE_RATES = [60, 30, 15]; // Hz 단위
    
    private currentLODLevel = 0;
    private accumulator = 0;

    update(deltaTime: number, distanceToCamera: number) {
        this.updateLODLevel(distanceToCamera);
        
        this.accumulator += deltaTime;
        const updateRate = this.PHYSICS_UPDATE_RATES[this.currentLODLevel];
        const timeStep = 1 / updateRate;

        if (this.accumulator >= timeStep) {
            this.accumulator -= timeStep;
            this.updatePhysics(timeStep);
        }
    }

    private updateLODLevel(distance: number) {
        for (let i = 0; i < this.LOD_DISTANCES.length; i++) {
            if (distance < this.LOD_DISTANCES[i]) {
                this.currentLODLevel = i;
                return;
            }
        }
        this.currentLODLevel = this.LOD_DISTANCES.length;
    }
}

4.2 물리 연산 최적화

class PhysicsOptimizer {
    private readonly SLEEP_THRESHOLD = 0.1;
    private readonly SLEEP_TIME = 3.0; // 초 단위
    
    private sleepTimer = 0;
    private isSimulating = true;

    update(deltaTime: number) {
        if (!this.isSimulating) return;

        const velocity = this.getRigidBodyVelocity();
        if (velocity.length() < this.SLEEP_THRESHOLD) {
            this.sleepTimer += deltaTime;
            if (this.sleepTimer >= this.SLEEP_TIME) {
                this.pauseSimulation();
            }
        } else {
            this.sleepTimer = 0;
        }
    }

    private pauseSimulation() {
        this.isSimulating = false;
        this.rigidBody.setEnabled(false);
    }

    onCollision() {
        if (!this.isSimulating) {
            this.isSimulating = true;
            this.rigidBody.setEnabled(true);
            this.sleepTimer = 0;
        }
    }
}

5. 네트워크 동기화

5.1 상태 동기화

interface VehicleState {
    position: Vector3;
    rotation: Quaternion;
    velocity: Vector3;
    angularVelocity: Vector3;
    wheelStates: WheelState[];
    damage: number;
}

class NetworkedVehicleController {
    private readonly STATE_BUFFER_SIZE = 10;
    private readonly INTERPOLATION_DELAY = 100; // ms
    
    private stateBuffer: VehicleState[] = [];
    private lastUpdateTime = 0;

    update(currentTime: number) {
        if (this.stateBuffer.length < 2) return;

        const interpolationTime = currentTime - this.INTERPOLATION_DELAY;
        let previousState: VehicleState | null = null;
        let nextState: VehicleState | null = null;

        // 보간할 상태 찾기
        for (let i = 0; i < this.stateBuffer.length - 1; i++) {
            if (this.stateBuffer[i].timestamp <= interpolationTime && 
                this.stateBuffer[i + 1].timestamp >= interpolationTime) {
                previousState = this.stateBuffer[i];
                nextState = this.stateBuffer[i + 1];
                break;
            }
        }

        if (previousState && nextState) {
            this.interpolateState(previousState, nextState, interpolationTime);
        }
    }

    private interpolateWheelStates(previous: WheelState[], next: WheelState[], alpha: number): WheelState[] {
        return previous.map((prevWheel, index) => ({
            rotation: prevWheel.rotation.slerp(next[index].rotation, alpha),
            suspensionLength: lerp(prevWheel.suspensionLength, next[index].suspensionLength, alpha),
            groundContact: alpha < 0.5 ? prevWheel.groundContact : next[index].groundContact
        }));
    }

    private applyState(state: VehicleState) {
        // 물리 엔진에 보간된 상태 적용
        this.rigidBody.setTranslation(state.position);
        this.rigidBody.setRotation(state.rotation);
        this.rigidBody.setLinvel(state.velocity);
        this.rigidBody.setAngvel(state.angularVelocity);

        // 휠 상태 업데이트
        state.wheelStates.forEach((wheelState, index) => {
            this.wheels[index].setRotation(wheelState.rotation);
            this.suspensions[index].setLength(wheelState.suspensionLength);
        });
    }
}

5.2 예측 및 보정 시스템

class PredictionSystem {
    private readonly PREDICTION_STEPS = 10;
    private readonly CORRECTION_THRESHOLD = 0.5; // 미터 단위
    private predictedStates: VehicleState[] = [];

    predictNextState(currentState: VehicleState, input: VehicleInput): VehicleState {
        let predictedState = this.cloneState(currentState);

        // 물리 시뮬레이션 스텝
        for (let i = 0; i < this.PREDICTION_STEPS; i++) {
            predictedState = this.simulatePhysicsStep(predictedState, input);
            this.predictedStates.push(predictedState);
        }

        return predictedState;
    }

    handleServerState(serverState: VehicleState) {
        const predictedState = this.predictedStates[0];
        const error = this.calculateError(predictedState, serverState);

        if (error > this.CORRECTION_THRESHOLD) {
            this.applySmoothedCorrection(serverState);
        }
    }

    private calculateError(predicted: VehicleState, actual: VehicleState): number {
        return predicted.position.distanceTo(actual.position);
    }

    private applySmoothedCorrection(serverState: VehicleState) {
        const CORRECTION_FACTOR = 0.3;
        const currentState = this.getCurrentState();

        // 보간된 수정 적용
        const correctedPosition = currentState.position.lerp(
            serverState.position, 
            CORRECTION_FACTOR
        );
        const correctedRotation = currentState.rotation.slerp(
            serverState.rotation, 
            CORRECTION_FACTOR
        );

        this.updateVehicleState({
            ...currentState,
            position: correctedPosition,
            rotation: correctedRotation
        });
    }
}

5.3 입력 지연 보상

class InputDelayCompensation {
    private readonly INPUT_BUFFER_SIZE = 30;
    private readonly MAX_INPUT_DELAY = 200; // ms
    
    private inputBuffer: VehicleInput[] = [];
    private inputSequenceNumber = 0;

    processInput(input: VehicleInput) {
        const timestampedInput = {
            ...input,
            sequence: this.inputSequenceNumber++,
            timestamp: Date.now()
        };

        this.inputBuffer.push(timestampedInput);
        this.trimBuffer();

        return this.predictStateWithInput(timestampedInput);
    }

    private predictStateWithInput(input: TimestampedInput): VehicleState {
        const currentState = this.getCurrentState();
        const deltaTime = (Date.now() - input.timestamp) / 1000;

        // 물리 시뮬레이션으로 예측된 상태 계산
        return {
            position: this.predictPosition(currentState, input, deltaTime),
            rotation: this.predictRotation(currentState, input, deltaTime),
            velocity: this.predictVelocity(currentState, input, deltaTime),
            wheelStates: this.predictWheelStates(currentState, input, deltaTime)
        };
    }

    private trimBuffer() {
        const currentTime = Date.now();
        this.inputBuffer = this.inputBuffer.filter(input => 
            currentTime - input.timestamp <= this.MAX_INPUT_DELAY
        );
    }
}

6. 성능 테스트 및 모니터링

class PerformanceMonitor {
    private metrics: {
        physicsTime: number[];
        frameTime: number[];
        networkLatency: number[];
    } = {
        physicsTime: [],
        frameTime: [],
        networkLatency: []
    };

    private readonly SAMPLE_SIZE = 100;

    measurePhysicsStep(callback: () => void) {
        const startTime = performance.now();
        callback();
        const endTime = performance.now();

        this.metrics.physicsTime.push(endTime - startTime);
        this.trimMetrics();
    }

    private trimMetrics() {
        Object.keys(this.metrics).forEach(key => {
            if (this.metrics[key].length > this.SAMPLE_SIZE) {
                this.metrics[key].shift();
            }
        });
    }

    getPerformanceReport(): PerformanceReport {
        return {
            averagePhysicsTime: this.calculateAverage(this.metrics.physicsTime),
            averageFrameTime: this.calculateAverage(this.metrics.frameTime),
            averageLatency: this.calculateAverage(this.metrics.networkLatency),
            physicsTimeP95: this.calculatePercentile(this.metrics.physicsTime, 95),
            frameTimeP95: this.calculatePercentile(this.metrics.frameTime, 95),
            latencyP95: this.calculatePercentile(this.metrics.networkLatency, 95)
        };
    }

    private calculatePercentile(array: number[], percentile: number): number {
        const sorted = [...array].sort((a, b) => a - b);
        const index = Math.ceil((percentile / 100) * sorted.length) - 1;
        return sorted[index];
    }
}

결론

이상의 구현을 통해 다음과 같은 이점을 얻을 수 있습니다

1. 현실적인 차량 물리 시뮬레이션

  • 지형에 따른 서스펜션 동작
  • 날씨 조건에 따른 물리적 변화
  • 차량 손상에 따른 성능 변화

2. 최적화된 성능

  • LOD 시스템을 통한 리소스 관리
  • 효율적인 물리 연산
  • 네트워크 지연 보상

3. 멀티플레이어 지원

  • 부드러운 상태 동기화
  • 예측 및 보정 시스템
  • 입력 지연 보상

이러한 기능들은 실제 게임이나 시뮬레이션에서 활용될 수 있으며, 필요에 따라 더 확장하거나 조정할 수 있습니다.

향후 개선 방향

1. AI 기반 물리 시뮬레이션 최적화

class AIPhysicsOptimizer {
    private readonly modelWeights: Float32Array;
    private readonly inputFeatures = 10;
    
    constructor() {
        // 사전 학습된 모델 가중치 로드
        this.modelWeights = this.loadModelWeights();
    }

    predictOptimalParameters(state: VehicleState): PhysicsParameters {
        // 현재 상태를 특징 벡터로 변환
        const features = this.extractFeatures(state);
        
        // 신경망을 통한 최적 파라미터 예측
        return {
            suspensionStiffness: this.predict(features, 'suspension'),
            dampingCoefficient: this.predict(features, 'damping'),
            frictionCoefficient: this.predict(features, 'friction')
        };
    }

    private extractFeatures(state: VehicleState): number[] {
        return [
            state.velocity.length(),
            state.angularVelocity.length(),
            state.suspension.compression,
            state.terrain.roughness,
            state.weather.condition,
            // ... 추가 특징들
        ];
    }
}

2. WebAssembly를 활용한 성능 향상

class WasmPhysicsEngine {
    private wasmInstance: WebAssembly.Instance;
    private memory: WebAssembly.Memory;

    async initialize() {
        const response = await fetch('physics_engine.wasm');
        const wasmModule = await WebAssembly.compile(await response.arrayBuffer());
        
        this.memory = new WebAssembly.Memory({ initial: 256 });
        this.wasmInstance = await WebAssembly.instantiate(wasmModule, {
            env: {
                memory: this.memory,
                log_number: (n: number) => console.log(n)
            }
        });
    }

    updatePhysics(deltaTime: number) {
        const ptr = this.allocateState();
        this.copyStateToWasm(ptr);
        
        (this.wasmInstance.exports.update_physics as Function)(ptr, deltaTime);
        
        this.copyStateFromWasm(ptr);
        this.deallocateState(ptr);
    }
}

3. 모바일 디바이스 지원 확장

class MobileOptimizer {
    private readonly MOBILE_QUALITY_LEVELS = {
        LOW: {
            physicsHz: 30,
            maxParticles: 100,
            shadowQuality: 0,
            maxDeformation: 5
        },
        MEDIUM: {
            physicsHz: 45,
            maxParticles: 250,
            shadowQuality: 1,
            maxDeformation: 10
        },
        HIGH: {
            physicsHz: 60,
            maxParticles: 500,
            shadowQuality: 2,
            maxDeformation: 20
        }
    };

    constructor(private quality: 'LOW' | 'MEDIUM' | 'HIGH') {
        this.applyQualitySettings();
    }

    private applyQualitySettings() {
        const settings = this.MOBILE_QUALITY_LEVELS[this.quality];
        
        // 물리 엔진 설정 적용
        PhysicsEngine.setUpdateRate(settings.physicsHz);
        ParticleSystem.setMaxParticles(settings.maxParticles);
        
        // 그래픽 설정 적용
        this.setupGraphics(settings);
    }

    private setupGraphics(settings: any) {
        const renderer = this.getRenderer();
        renderer.shadowMap.enabled = settings.shadowQuality > 0;
        renderer.setPixelRatio(window.devicePixelRatio * 0.8);
    }
}

4. VR/AR 환경 대응

class VRVehicleController {
    private readonly HAND_GRIP_THRESHOLD = 0.7;
    private readonly STEERING_SENSITIVITY = 1.5;

    constructor(private xrSession: XRSession) {
        this.initializeVRControls();
    }

    private initializeVRControls() {
        this.xrSession.addEventListener('inputsourceschange', this.onInputSourcesChange);
        
        // VR 컨트롤러 설정
        this.setupControllers();
    }

    private setupControllers() {
        const leftController = this.xrSession.inputSources[0];
        const rightController = this.xrSession.inputSources[1];

        // 핸들링 컨트롤
        this.setupSteeringWheel(leftController, rightController);
        
        // 가속/제동 컨트롤
        this.setupPedals(rightController);
    }

    private updateVRControls(frame: XRFrame) {
        const pose = frame.getInputPose(this.xrSession.inputSources[0]);
        if (pose) {
            // 핸들 회전 계산
            const rotation = this.calculateSteeringRotation(pose);
            this.updateVehicleSteering(rotation);
            
            // 가속/제동 상태 업데이트
            this.updateThrottleAndBrake(frame);
        }
    }
}

5. 테스트 자동화 시스템

class PhysicsTestSuite {
    private testCases: PhysicsTestCase[] = [];
    private results: TestResult[] = [];

    async runAllTests() {
        for (const testCase of this.testCases) {
            const result = await this.runTest(testCase);
            this.results.push(result);
            
            if (!result.success) {
                console.error(`Test failed: ${testCase.name}`, result.error);
            }
        }

        this.generateReport();
    }

    private async runTest(testCase: PhysicsTestCase): Promise<TestResult> {
        const vehicle = new Vehicle(testCase.initialState);
        const startTime = performance.now();

        try {
            // 테스트 시나리오 실행
            for (const action of testCase.actions) {
                await this.executeAction(vehicle, action);
            }

            // 결과 검증
            const endState = vehicle.getState();
            const success = this.verifyResults(endState, testCase.expectedState);

            return {
                name: testCase.name,
                success,
                duration: performance.now() - startTime,
                error: null
            };
        } catch (error) {
            return {
                name: testCase.name,
                success: false,
                duration: performance.now() - startTime,
                error
            };
        }
    }
}
profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글