Skip to content

daminoworld/SharpMemory

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SharpMemory

Apple Vision Pro에서 사진 한 장으로 기억속 공간을 몰입형 3D 공간으로 만드는 R&D 프로젝트

image 125

License: MIT Platform

Apple Research가 2025년 12월 공개한 SHARP 모델로 단일 이미지를 1.18M개의 3D Gaussian Splats로 변환하고, Vision Pro에서 몰입형 공간으로 렌더링/인터랙션할 수 있는 visionOS 앱입니다.

⚠️ 개인 R&D 프로젝트입니다. visionOS에서의 single-image 3DGS 통합과 1.18M Splats 환경의 GPU 렌더링 안정화를 탐구하는 과정의 결과물입니다.


Table of Contents


Demo

SharpMemoryDemo-2.mp4
  • 사진 선택 → 변환 진행 → 몰입형 공간 배치 → 핸드 제스처 조작

Project Scope

이 프로젝트에서 개발,통합한 영역:

  • visionOS 클라이언트 앱: Gallery UI, 변환 진행률 표시(SSE), MetalSplatter 통합, Hand Gesture
  • FastAPI 추론 서버: SHARP 모델 wrapper, 비동기 작업 큐, 진행률 스트리밍
  • MetalSplatter 최적화: visionOS 환경에서의 렌더링 안정화, Sort 파이프라인 개선

활용한 외부 자산:

  • SHARP Model: Apple Research가 공개한 single-image 3DGS 생성 모델
  • MetalSplatter Library: scier가 공개한 Apple 플랫폼용 3DGS 렌더러 (로컬 패키지로 적용 후 수정)

Technical Deep Dive

1.18M Gaussian Splats 렌더링 떨림 디버깅

문제와 진단

앱 렌더링 (GPU)  →  Compositor  →  Reprojection  →  디스플레이 (90Hz)
      ↓                               ↓
색상 + depth 버퍼 생성          머리 움직임에 맞춰
                               이전 프레임을 depth 기반으로 보정

문제: 1.18M Splats 렌더링 시 머리 움직임에 따라 모델이 떨리고 일시적으로 사라지는 현상 발생.

진단:

  • 화면 녹화로 떨림이 잡히지 않음 → 렌더링된 프레임 자체는 정상
  • → Compositor의 Reprojection 단계에서 발생하는 아티팩트로 판단
  • 프레임 예산 11.11ms (90fps) 대비 프레임 처리 시간 55 ~ 91ms로 5~9배 초과

visionOS는 앱 렌더 시점과 디스플레이 시점 사이의 머리 움직임을 보정하기 위해 depth 버퍼 기반으로 픽셀 위치를 조정(Reprojection). GPU가 렌더링에 너무 오래 걸리면 오래된 depth가 reprojection에 사용되어 보정이 부정확해지고, 이것이 떨림으로 나타남.


1단계 — 파이프라인 파라미터 조정 (실패)

MetalSplatter 라이브러리의 내부 설정과 SampleApp을 비교하며 파라미터 차이를 추적.

비교한 항목:

  • highQualityDepth (depth 버퍼 정밀도): true/false 전환 → 둘 다 떨림
  • sortTimeout (정렬 완료 대기 시간): 0.0/0.1 등 변경 → 효과 없음
  • encodePresent (프레임 제출 조건): 렌더 스킵 시에도 제출하도록 변경 → 효과 없음

다른 환경 변수도 배제:

  • SampleApp 로컬 빌드도 동일하게 떨림 → 라이브러리 코드 차이 아님
  • TestFlight 배포 빌드도 동일 → App Store 빌드 파이프라인 문제 아님

이 단계에서 파라미터 레벨 해결이 불가능하다고 판단하고 렌더링 부하 자체를 줄이는 방향으로 전환.


2단계 — Splat 수 감축, 두 접근의 한계

가설: 렌더링 대상 Splat 수를 줄이면 GPU 부하가 감소하여 떨림 해결.

접근 방법 결과 한계
A. Scale-based Pruning 로드 시 size³ 기준으로 작은 Splat 영구 제거 (1.18M → 500K) 떨림 해결, FPS 30 → 90 디테일 손실
B. View-Dependent Importance Capping 매 sort마다 opacity / distance² 기준 상위 Splat만 GPU 전달 가까운 디테일 복원 먼 배경 사라짐

핵심 발견: 떨림의 근본 원인은 "GPU 렌더링 처리 시간이 프레임 예산을 초과하는 것" 이며, Splat 수 감축이 해결책. 그러나 "무엇을 기준으로 제거할지" 가 새로운 문제.

왜 Scale Pruning이 디테일을 잃었나: 텍스트를 구성하는 splat은 크기가 작고 불투명도가 높은 특성이 있는데, 순수 볼륨 기준 pruning에서 가장 먼저 제거되는 대상이었음.

opacity / distance²가 배경을 잃었나: Inverse-square law(1/r²)는 거리 제곱에 반비례하므로, 10m 거리의 splat은 1m 거리의 splat보다 importance가 100배 낮게 계산됨. 결과적으로 가까운 splat이 예산을 독점하여 먼 배경 splat이 완전히 밀려남.


3단계 — Screen-Space Projection Metric, 두 접근의 통합

방향 전환: "크기""거리" 중 하나가 아닌 "화면상 실제 차지하는 면적" 을 기준으로 삼음.

접근 A: size³ (volume)
접근 B: opacity / distance²
통합:   opacity × size² / distance²

수학적 근거: 3D 공간의 splat이 화면(2D)에 투영될 때 차지하는 면적은 원근 효과(perspective projection)에 의해 size² / distance²에 비례. 거리가 2배 멀어지면 화면 면적은 1/4로 감소.

Covariance trace로 size² 근사: 3D Gaussian Splatting에서 각 splat의 모양은 3D covariance 행렬로 인코딩되어 있음. 원래의 scale 값은 covariance에 합쳐져 저장되므로 대신 trace(대각합) = s_x² + s_y² + s_z²로 size²의 합을 얻을 수 있음.

let covTrace = Float(splat.covA.x) + Float(splat.covB.x) + Float(splat.covB.z)
let importance = Float(splat.colorSH0.a) * covTrace / max(distSq, 0.01)
Splat 유형 기존 metric 새 metric
큰 배경 (먼 거리) 낮음 (제거됨) 높음 (보존)
작은 디테일 (가까이) 높음 여전히 높음
작고 먼 splat 낮음 가장 낮음 (우선 제거)

결과: 근거리 디테일 유지 + 배경 보존을 동시에 개선.


동시 적용한 최적화

Sort 파이프라인 C++로 재작성

기존 Swift Array.sort의 closure 기반 비교는 매 비교마다 witness table을 경유하는 간접 호출 발생. 1.18M 비교 × indirect call 오버헤드.

C++ std::sort로 교체하고 UInt64 packed key를 설계하여 5~6x 개선.

flipFloat — Float을 정수 비교 가능하게 변환:

private static func flipFloat(_ f: Float) -> UInt32 {
    let u = f.bitPattern
    let mask: UInt32 = (u & 0x8000_0000 != 0) 
        ? 0xFFFF_FFFF    // 음수: 모든 비트 반전
        : 0x8000_0000    // 양수: 부호 비트만 반전
    return u ^ mask
}

IEEE 754 Float의 비트 패턴은 양수 범위에서는 정수 비교와 순서가 일치하지만, 음수에서는 뒤집힘. flipFloat으로 변환하면 모든 범위에서 UInt32 정수 비교만으로 Float 정렬이 가능.

UInt64 packed key — 정렬 기준 + 인덱스를 8바이트에 압축:

let bits = UInt64(flipFloat(importance))
let key = (bits << 32) | UInt64(flatIndex)

상위 32비트에 importance, 하위 32비트에 원본 인덱스를 packing. Sort 후 하위 비트만 추출하여 인덱스 복원. 별도 인덱스 배열 재배치가 불필요.

캐시 효율 개선: 기존 32B struct × 1.18M = 37.8MB (M2 L2 캐시 16MB 초과) → 8B UInt64 × 1.18M = 9.4MB (L2에 fit). Cache thrashing 해소.

항목 Before After
Sort 시간 122~175ms 26~29ms
비교 단위 32B struct + closure 8B UInt64 정수 비교
캐시 적합성 37.8MB (L2 초과) 9.4MB (L2 fit)

HandTrackingProvider → Spatial Events API 대체

ARKit HandTrackingProvider를 ARSession에 등록하면 시스템이 매 카메라 프레임마다 ML 파이프라인(손 감지 + 26개 관절 추정)을 실행. 이 파이프라인이 GPU compute unit을 점유.

진단 과정:

  • HandTrackingProvider 비활성화 → 떨림 큰 폭 개선 (원인 확인)
  • 별도 Task로 분리해서 읽기 → 여전히 떨림 → ARSession 등록 자체가 부하 원인

해결: visionOS Compositor 내장의 onSpatialEvent API를 사용. 시스템이 이미 수행하고 있는 핸드 트래킹을 재사용하므로 추가 ML 파이프라인 활성화 없이 핀치 이벤트 수신 가능.


측정 결과 (Instruments Metal System Trace)

Stage Total 비율 Max Duration
Vertex 1.13 min 49% 25.37 ms
Fragment 55.93 s 40% 15.74 ms
Compute 15.23 s 11% 18.70 ms

Vertex와 Fragment 복합 병목. 어느 한쪽만 최적화해서는 정면 전체 시야(0% culled)에서 90 FPS 안정 달성 불가능.


시도했으나 폐기한 접근

Compute Pre-Pass: Vertex shader의 per-splat 중복 연산을 compute kernel로 분리하는 가설을 세우고 226MB ring buffer + per-splat 연산 분리를 구현. Instruments 측정 결과 FPS 변화 없음을 확인하고 롤백. Apple GPU의 vertex caching이 이미 중복 연산을 최적화하고 있었음.


Roadmap

  • RunPod 등 클라우드 GPU 서버 연동 (현재 Mac 로컬 MPS → NVIDIA CUDA로 추론 속도 개선)

Architecture

graph LR
    A["Vision Pro<br/>(Photos)"] -->|Upload Image| B["FastAPI Server"]
    B -->|SHARP Inference| C["3D Gaussians<br/>(.ply)"]
    C -->|Download| D["SharpMemory App"]
    D -->|MetalSplatter| E["Immersive 3D View"]
    E -->|Hand Tracking| F["Rotate / Scale / Move"]
Loading

Phase 1 — 변환 (네트워크): 이미지 업로드 → SHARP 추론 → .ply 다운로드 Phase 2 — 렌더링 (로컬): .ply 로드 → MetalSplatter → 몰입형 공간 + Hand Gesture


Tech Stack

Layer Technologies
Client Swift 6, visionOS 2+, Metal, MetalSplatter, ARKit, CompositorServices, SwiftUI
Server Python, FastAPI, PyTorch, MPS (Apple Silicon)
ML Model SHARP (Apple Research) — single-image to 3D Gaussian Splatting

References


Getting Started

로컬에서 실행하려면 펼쳐보기

Prerequisites

  • Client: Xcode 16+, visionOS 2+ SDK, Apple Vision Pro (or Simulator)
  • Server: Python 3.11+, Apple Silicon Mac (MPS) or NVIDIA GPU (CUDA)
  • ML Model: ml-sharp cloned locally

Server Setup

cd server

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Set path to ml-sharp source (required)
export ML_SHARP_PATH=/path/to/ml-sharp/src

# Start server
uvicorn main:app --host 0.0.0.0 --port 8000

The model checkpoint (~700MB) downloads automatically on first run.

Client Setup

  1. Open client/SharpMemory.xcodeproj in Xcode
  2. Select your development team in Signing & Capabilities
  3. Simulator: Server URL defaults to http://localhost:8000
  4. Device: Create client/SharpMemory/DevConfig.swift (gitignored) to point at your server:
    enum DevConfig {
        static let serverBaseURL: String? = "http://<YOUR_MAC_IP>:8000"
    }
    Find your Mac's IP via System Settings → Wi-Fi → Details → IP Address.
  5. Build and run (use Release mode for optimal rendering performance)

API Reference

Method Endpoint Description
POST /api/predict Upload image, returns job_id
GET /api/status/{job_id} Poll processing status (0.0–1.0 progress)
GET /api/stream/{job_id} Server-Sent Events for real-time progress
GET /api/download/{job_id} Download generated PLY file
GET /health Health check

License

This project is licensed under the MIT License — see LICENSE for details.

The SHARP model and ml-sharp code are subject to Apple's license. MetalSplatter is used under its original license.

About

Turn a single photo into an immersive 3D space on Apple Vision Pro — visionOS app with Apple's SHARP model + 1.18M+ Gaussian Splats rendering

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors