PBD Soft Body

Position Based Dynamics - VR 수술 시뮬레이션을 위한 실시간 소프트바디 물리 엔진

2025.01 | 개발 기간 4주

기술 스택

UnityC#Meta QuestJob SystemBurst CompilerPosition Based DynamicsShape Matching

프로젝트 개요

이 프로젝트는 VR 수술 시뮬레이션에서 장기 조직의 부드러운 변형을 실시간으로 구현하는 것이 목표였습니다. 기존 Unity Physics는 Rigidbody만 지원하며 소프트바디를 기본적으로 지원하지 않아, 메쉬 변형이 불가능했습니다.

VR 환경에서는 최소 72fps를 유지해야 하며, Quest 2/3 같은 모바일 환경에서도 작동해야 했습니다. 자연스러운 물렁물렁한 느낌, 중력과 충돌 반응, 그리고 VR Grab과의 완벽한 통합이 필요했습니다.


해결해야 할 문제

기존 Unity Physics의 한계

Unity의 기본 물리 엔진은 단단한 물체(Rigidbody)만 지원합니다.

  • Rigidbody는 단단한 물체만 표현 가능
  • 소프트바디 기본 지원 없음
  • 메쉬 변형 불가능
  • 조직의 탄성이나 부드러움을 표현할 수 없음

성능 요구사항

VR 환경에서는 매우 엄격한 성능 제약이 있습니다.

  • VR 환경: 최소 72fps 유지 필수
  • 실시간 상호작용 필수 - 지연이 있으면 멀미 유발
  • Quest 2/3 모바일 환경에서 작동
  • 제한된 CPU/GPU 리소스

구현 목표

  • 자연스러운 물렁물렁한 느낌
  • 중력, 충돌, 마찰에 대한 현실적인 반응
  • VR Grab 시스템과 완벽한 통합
  • 메쉬 절단 시스템과 호환

핵심 과제

"기존 물리 엔진으로는 부드러운 조직의 실시간 시뮬레이션이 불가능 → 새로운 접근 필요"


개발 과정 - 4번의 시도

완벽한 솔루션을 찾기까지 4번의 시도를 거쳤습니다. 각 시도마다 문제점을 파악하고 개선하는 과정을 통해 최종 솔루션에 도달했습니다.

1

Spring-Mass System

실패 - 폭발

접근 방법:

  • 파티클들을 스프링으로 연결
  • Hooke's Law 적용: F = -k × Δx
  • 질량-스프링 모델로 구현

왜 이 방법을 선택했나:

  • 직관적: 물리학 101 - Hooke's Law
  • 구현 쉬움: 간단한 공식
  • 널리 사용: 천 시뮬레이션의 기본
  • 다른 대안들:
    • FEM (Finite Element Method): 너무 복잡, 실시간 불가능
    • Bullet Physics: Unity 통합 어려움
    • Havok: 라이센스 문제

문제점:

  • 불안정함: 스프링 상수(stiffness) 조절이 지옥같음
  • 폭발 현상: Time step에 매우 민감
  • 성능 문제: 매 프레임 수천 개 스프링 계산
  • Stiffness 높이면 → 폭발
  • Stiffness 낮추면 → 흐물흐물

배운 점:

물리학적으로는 맞지만 수치 안정성이 최악이었습니다. 다른 접근이 필요했습니다.

Tetrahedral Mesh (검토 후 포기)

포기 - 너무 무거움

의료 시뮬레이션 표준:

  • SOFA Framework (프랑스 표준)
  • 3D Slicer (의료 영상 처리)
  • NVIDIA Flex Medical

왜 업계 표준인가:

  • 정확한 부피 보존: 내부 구조 포함
  • 내부 응력 계산: FEM 최적화
  • 의학적 정확도: 실제 조직 표현

왜 포기했는가:

  • 파티클 수 폭발: 500개 → 5,000~10,000개 (10-20배)
  • 연산량 증가: Distance Constraints 10배
  • 메모리 문제: Quest 2/3 제한
  • 렌더링 복잡: 내부 숨기고 표면만 추출 필요
  • VR 72fps 달성 불가능

대신 선택한 방법:

  • Surface Mesh: 껍데기만 사용
  • Shape Matching: 부피 보존 근사
  • 실시간 가능: 72fps 안정적 달성
  • 파티클 500개로 충분

배운 점:

의료 표준이 항상 VR에 최적은 아닙니다. 목적에 맞는 선택이 중요했습니다.

2

Verlet Integration

실패 - 형태 손실

개선된 아이디어:

  • 속도 없이 위치만으로 시뮬레이션
  • p_new = 2p - p_old + a·dt²
  • 암시적 속도 계산

왜 Verlet을 선택했나:

  • 안정성 향상: Spring-Mass보다 덜 폭발
  • 간단함: 속도 변수 불필요
  • 에너지 보존: 수치 오차 적음
  • 다른 방법들:
    • Euler Integration: 여전히 불안정
    • Runge-Kutta: 너무 느림 (4단계)
    • Implicit Methods: 구현 복잡

문제점:

  • 형태 유지 어려움: 쭉쭉 늘어남
  • 제약 조건 부족: 원래 모양으로 안 돌아옴
  • Constraint Solving 필요: 거리 제약만으로는 부족
  • 전체적으로 스파게티처럼 됨

배운 점:

안정성은 개선됐지만 형태 보존에 실패했습니다. Constraint 시스템이 필요했습니다.

3

Distance Constraints Only

부분 성공

단순화된 접근:

  • Verlet + 거리 제약만 사용
  • 각 엣지의 길이만 유지
  • 반복적으로 보정 (Iterative Solving)

왜 Distance Constraints를 선택했나:

  • 단순함: 엣지 길이만 유지
  • 빠름: O(n) 복잡도
  • 작동함: 더 이상 폭발 안 함
  • 대안 제약들:
    • Bending Constraints: 구현 복잡, 느림
    • Volume Constraints: 부피 계산 오버헤드
    • Area Preservation: 삼각형마다 계산

성공한 점:

  • 안정적으로 작동: 폭발 없음
  • 72fps 달성: 실시간 가능
  • 엣지 길이 유지: 구조적 강성 확보

문제점:

  • 전체 형태 손실: 부분은 유지, 전체는 뭉개짐
  • Shape Matching 필요: 글로벌 제약 부재
  • 엣지는 OK, 전체 형태는 NG
  • 납작해지거나 비틀림

배운 점:

Local Constraints는 성공했지만, Global Constraint가 필요했습니다. Shape Matching을 추가해야 했습니다.

💡

Breakthrough: PBD + Shape Matching

성공!

결정적 발견

Local Constraints (Distance): 엣지 길이 유지 +
Global Constraint (Shape Matching): 전체 형태 복원
= 완벽한 소프트바디 시뮬레이션!

Shape Matching 원리:

  • 원래 형태 기억: Rest Configuration 저장
  • Polar Decomposition: 회전 행렬 추출
  • 목표 위치 계산: 원래 형태로 당김

왜 완벽한가:

  • 안정적: 폭발 없음
  • 형태 유지: 원래 모양으로 복원
  • 빠름: 72fps 달성
  • 자연스러움: 부드러운 변형
  • 모든 문제 해결!

핵심 파라미터:

  • DistanceStiffness: 0.9 (엣지 강도)
  • ShapeMatchingStiffness: 0.1 (형태 복원)
  • NumSubsteps: 5 (안정성)
  • Mass: 1.0 (파티클 질량)

핵심 인사이트:

Local + Global의 완벽한 조합이 Position Based Dynamics의 핵심이었습니다!


최종 솔루션

4번의 시도를 통해 얻은 교훈을 모두 결합하여 완성된 PBD + Shape Matching 시스템입니다.

핵심 기술 3가지

1. Position Based Dynamics (PBD)

위치 기반 동역학은 속도가 아닌 위치를 직접 조작하여 안정성을 확보합니다. Verlet Integration으로 위치를 예측하고, Constraint Solving으로 보정하는 2단계 과정입니다. 매 프레임마다 파티클의 위치를 업데이트하고, Distance Constraints로 엣지 길이를 유지합니다.

2. Shape Matching (Polar Decomposition)

원래 형태를 기억하고 복원하는 글로벌 제약입니다. 질량중심을 계산하고, 변형 행렬을 Polar Decomposition으로 분해하여 순수 회전 성분만 추출합니다. 이를 통해 원래 형태로 부드럽게 복원하면서도 유연한 변형을 허용합니다.

3. Unity Job System + Burst Compiler

멀티스레드 병렬 처리와 SIMD 최적화로 8배 성능 향상을 달성했습니다. IJobParallelFor로 파티클 계산을 병렬화하고, Burst Compiler로 CPU SIMD 명령어를 활용했습니다. NativeArray를 사용하여 GC를 완전히 제거했습니다.

핵심 알고리즘 코드

Shape Matching 수학

csharp
// 1. 질량중심 계산
float3 cm = float3.zero;
for (int i = 0; i < numParticles; i++)
{
    cm += positions[i] * masses[i];
}
cm /= totalMass;

// 2. 변형 행렬 A 계산
float3x3 A = float3x3.zero;
for (int i = 0; i < numParticles; i++)
{
    float3 p = positions[i] - cm;
    float3 q = restPositions[i];
    A += math.mul(p, math.transpose(q)) * masses[i];
}

// 3. Polar Decomposition (회전 행렬 추출)
float3x3 R = ExtractRotation(A); // SVD 또는 반복법

// 4. 목표 위치 계산
for (int i = 0; i < numParticles; i++)
{
    float3 goal = cm + math.mul(R, restPositions[i]);
    positions[i] += (goal - positions[i]) * shapeMatchingStiffness;
}

Job System 병렬 처리

csharp
[BurstCompile]
struct PredictPositionsJob : IJobParallelFor
{
    public NativeArray<float3> Positions;
    public NativeArray<float3> PrevPositions;
    [ReadOnly] public float3 Gravity;
    [ReadOnly] public float DeltaTime;

    public void Execute(int index)
    {
        float3 velocity = (Positions[index] - PrevPositions[index]) / DeltaTime;
        PrevPositions[index] = Positions[index];
        
        // Verlet Integration
        Positions[index] += velocity * DeltaTime + Gravity * DeltaTime * DeltaTime;
    }
}

// 실행
new PredictPositionsJob { /* ... */ }.Schedule(numParticles, 64).Complete();

전체 알고리즘 흐름

  1. Predict: Verlet Integration으로 위치 예측 (중력 적용)
  2. Distance Constraints: 엣지 길이 유지 (5-10회 반복)
  3. Shape Matching: 전체 형태를 원래 모양으로 복원
  4. Collision: 바닥, 벽 등 환경과 충돌 처리
  5. Update Velocity: 위치 변화로부터 속도 계산
  6. Apply Damping: 감쇠 적용하여 과도한 진동 방지
  7. Update Mesh: 계산된 위치로 렌더링 메쉬 업데이트

핵심 성공 요인

Local (Distance) + Global (Shape Matching)의 조합이 모든 것을 해결했습니다. 안정성, 형태 유지, 성능을 모두 달성했습니다.

특히 Job System + Burst Compiler의 최적화로 8배 성능 향상을 이루어 VR 환경에서 안정적인 72fps를 유지할 수 있었습니다.


기술 상세 설명

Unity Job System의 위력

Unity의 Job System은 멀티스레드 병렬 처리를 안전하고 쉽게 구현할 수 있게 해줍니다.

단일 스레드 (기존):

  • 모든 파티클을 순차적으로 처리
  • 500개 파티클 × 5 substeps = 2500번 계산
  • 결과: 16ms (60fps 불가)

멀티 스레드 (Job System):

  • CPU 코어 수만큼 병렬 처리
  • 8코어 CPU에서 8배 빠름
  • 결과: 2ms (500fps 가능!)

Burst Compiler의 마법

Burst Compiler는 C# 코드를 고도로 최적화된 네이티브 코드로 컴파일합니다.

  • SIMD (Single Instruction Multiple Data): 4개 파티클을 동시에 처리
  • Auto Vectorization: 자동으로 벡터 연산 최적화
  • 최대 10배 성능 향상
  • 단순히 [BurstCompile] 속성만 추가하면 됨

NativeArray로 GC 제로 달성

일반 C# 배열은 매 프레임 GC(Garbage Collection)를 발생시켜 프레임 드랍을 유발합니다.

일반 배열 (GC 발생):

  • Vector3[] positions - 관리되는 메모리
  • 매 프레임 GC 체크
  • 불규칙한 프레임 드랍

NativeArray (GC 없음):

  • NativeArray<float3> - 네이티브 메모리
  • Zero GC - 완전히 관리 밖
  • 안정적인 프레임레이트

VR Integration: Rigidbody + PBD 통합

가장 어려웠던 부분은 Unity의 Rigidbody 물리와 PBD를 통합하는 것이었습니다.

구조:

  • Parent: Rigidbody + Collider (중력, 충돌 담당)
  • Child: PBD Soft Body (메쉬 변형 담당)

핵심 문제:

  • PBD는 월드 좌표계에서 독립적으로 동작
  • 부모가 이동해도 PBD에 전달 안 됨
  • 중력이 이중으로 적용됨

해결책:

  • Parent Motion Tracking: 부모 Transform 변화 추적하여 모든 파티클에 적용
  • Gravity Delegation: PBD 자체 중력 끄고 Rigidbody 중력만 사용
  • Collision Integration: Rigidbody 충돌을 PBD Plane으로 동적 추가

프로젝트 성과

72+

FPS

Quest 2/3에서 안정적인 프레임레이트

8배

성능 향상

Job System + Burst로 16ms → 2ms

4주

개발 기간

4번의 시도 끝에 완성

성능 비교

방법처리 시간FPS결과
단일 스레드 (일반 C#)16ms60fps 불가VR 불가능
Job System4ms250fps개선
Job + Burst (최종)2ms500fps완벽

핵심 배움

  • 물리 시뮬레이션의 복잡성: Spring-Mass에서 PBD까지, 4번의 시도를 통해 점진적으로 문제를 해결
  • 최적화의 중요성: 알고리즘도 중요하지만 구현 최적화가 더 중요함. Job System + Burst로 8배 향상
  • VR 통합의 어려움: Rigidbody와 PBD의 조화, 좌표계 변환, 실시간 성능 유지
  • 실패를 통한 학습: 4번의 실패가 있었기에 최종 솔루션이 완성될 수 있었음
  • 목적에 맞는 선택: Tetrahedral Mesh 같은 의료 표준도 VR에는 과할 수 있음

향후 계획

  • Collision Mesh 개선: 더 정확한 충돌 처리를 위한 Collision Mesh 최적화
  • Self-Collision 추가: 파티클 간 충돌 처리로 더 현실적인 시뮬레이션
  • Haptic Feedback: VR 컨트롤러 햅틱 피드백 연동으로 촉각 전달
  • 메쉬 절단과 완벽 통합: Mesh Slicing 시스템과 결합하여 절단된 조직도 물리 시뮬레이션
  • 다양한 조직 특성: 간, 심장, 폐 등 각 장기의 고유한 물성 구현

© 2026 Trout. Built with Next.js & TypeScript.