480 lines
13 KiB (Stored with Git LFS)
Markdown
480 lines
13 KiB (Stored with Git LFS)
Markdown
# 기술 사양서 (Technical Specifications)
|
|
|
|
## 🏗️ 시스템 아키텍처
|
|
|
|
### 전체 시스템 구조
|
|
```
|
|
[모션캡쳐 스튜디오] → [Unity 서버] → [WebSocket] → [웹 클라이언트]
|
|
↓ ↓ ↓
|
|
모션 데이터 수집 실시간 스트리밍 3D 렌더링 + 제어
|
|
```
|
|
|
|
### 데이터 흐름
|
|
1. **모션캡쳐 → Unity**: Mocap 장비에서 Unity로 실시간 데이터 입력
|
|
2. **Unity → Web**: WebSocket을 통한 실시간 아바타 Transform 전송
|
|
3. **Web → Unity**: 녹화 명령 및 제어 신호 전송
|
|
4. **Web**: 로컬 모션 데이터 저장 및 파일 생성
|
|
|
|
## 📡 통신 프로토콜
|
|
|
|
### WebSocket 메시지 포맷
|
|
|
|
#### 1. 모션 데이터 (Unity → Web)
|
|
```json
|
|
{
|
|
"type": "motion_frame",
|
|
"timestamp": 1640995200000,
|
|
"frameRate": 30,
|
|
"avatar": {
|
|
"bones": [
|
|
{
|
|
"name": "Hips",
|
|
"position": {"x": 0.0, "y": 1.0, "z": 0.0},
|
|
"rotation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
|
|
"scale": {"x": 1.0, "y": 1.0, "z": 1.0}
|
|
},
|
|
{
|
|
"name": "Spine",
|
|
"position": {"x": 0.0, "y": 1.2, "z": 0.0},
|
|
"rotation": {"x": 0.1, "y": 0.0, "z": 0.0, "w": 0.995}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. 녹화 제어 명령 (Web → Unity)
|
|
```json
|
|
{
|
|
"type": "recording_command",
|
|
"command": "start_recording",
|
|
"parameters": {
|
|
"filename": "motion_capture_001",
|
|
"format": "fbx",
|
|
"frameRate": 30,
|
|
"duration": 300
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. 상태 응답 (Unity → Web)
|
|
```json
|
|
{
|
|
"type": "recording_status",
|
|
"status": "recording",
|
|
"filename": "motion_capture_001.fbx",
|
|
"duration": 45.2,
|
|
"frameCount": 1356,
|
|
"fileSizeKB": 2048
|
|
}
|
|
```
|
|
|
|
### 에러 처리
|
|
```json
|
|
{
|
|
"type": "error",
|
|
"code": "RECORDING_FAILED",
|
|
"message": "녹화 파일 생성에 실패했습니다.",
|
|
"details": {
|
|
"cause": "disk_space_insufficient",
|
|
"timestamp": 1640995200000
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🎮 Unity 구현 사양
|
|
|
|
### 핵심 컴포넌트
|
|
|
|
#### 1. WebSocketServer.cs
|
|
```csharp
|
|
public class WebSocketServer : MonoBehaviour
|
|
{
|
|
[SerializeField] private int port = 8080;
|
|
[SerializeField] private float updateRate = 30f;
|
|
|
|
private WebSocketSharp.Server.WebSocketServer server;
|
|
private MotionDataStreamer streamer;
|
|
|
|
public void StartServer() { /* 서버 시작 */ }
|
|
public void StopServer() { /* 서버 종료 */ }
|
|
public void BroadcastMotionData(MotionFrame frame) { /* 데이터 전송 */ }
|
|
}
|
|
```
|
|
|
|
#### 2. MotionDataStreamer.cs
|
|
```csharp
|
|
public class MotionDataStreamer : MonoBehaviour
|
|
{
|
|
[SerializeField] private Transform avatarRoot;
|
|
[SerializeField] private string[] boneNames;
|
|
|
|
private Dictionary<string, Transform> boneTransforms;
|
|
|
|
public MotionFrame CaptureCurrentFrame()
|
|
{
|
|
// 현재 아바타 상태를 MotionFrame으로 변환
|
|
}
|
|
|
|
public void SendMotionData()
|
|
{
|
|
// 실시간으로 모션 데이터 전송
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. RemoteMotionRecorder.cs
|
|
```csharp
|
|
public class RemoteMotionRecorder : MonoBehaviour
|
|
{
|
|
private EasyMotionRecorder motionRecorder;
|
|
private bool isRecording = false;
|
|
|
|
public void StartRecording(string filename) { /* 녹화 시작 */ }
|
|
public void StopRecording() { /* 녹화 종료 */ }
|
|
public RecordingStatus GetStatus() { /* 상태 조회 */ }
|
|
}
|
|
```
|
|
|
|
### 데이터 구조
|
|
```csharp
|
|
[System.Serializable]
|
|
public class MotionFrame
|
|
{
|
|
public long timestamp;
|
|
public float frameRate;
|
|
public BoneData[] bones;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class BoneData
|
|
{
|
|
public string name;
|
|
public Vector3 position;
|
|
public Quaternion rotation;
|
|
public Vector3 scale;
|
|
}
|
|
```
|
|
|
|
## 🌐 웹 클라이언트 사양
|
|
|
|
### 핵심 모듈
|
|
|
|
#### 1. WebSocketClient.js
|
|
```javascript
|
|
class WebSocketClient {
|
|
constructor(url) {
|
|
this.url = url;
|
|
this.socket = null;
|
|
this.onMotionData = null;
|
|
this.onStatusUpdate = null;
|
|
}
|
|
|
|
connect() {
|
|
this.socket = new WebSocket(this.url);
|
|
this.setupEventHandlers();
|
|
}
|
|
|
|
sendCommand(command) {
|
|
this.socket.send(JSON.stringify(command));
|
|
}
|
|
|
|
setupEventHandlers() {
|
|
this.socket.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
this.handleMessage(data);
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. MotionViewer.js
|
|
```javascript
|
|
class MotionViewer {
|
|
constructor(containerId) {
|
|
this.scene = new THREE.Scene();
|
|
this.camera = new THREE.PerspectiveCamera();
|
|
this.renderer = new THREE.WebGLRenderer();
|
|
this.avatar = null;
|
|
this.bones = new Map();
|
|
}
|
|
|
|
loadAvatar(modelPath) {
|
|
// 3D 아바타 모델 로드
|
|
}
|
|
|
|
updateMotion(motionFrame) {
|
|
// 실시간 모션 데이터 적용
|
|
motionFrame.bones.forEach(bone => {
|
|
const boneObject = this.bones.get(bone.name);
|
|
if (boneObject) {
|
|
boneObject.position.set(bone.position.x, bone.position.y, bone.position.z);
|
|
boneObject.quaternion.set(bone.rotation.x, bone.rotation.y, bone.rotation.z, bone.rotation.w);
|
|
}
|
|
});
|
|
}
|
|
|
|
render() {
|
|
this.renderer.render(this.scene, this.camera);
|
|
requestAnimationFrame(() => this.render());
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. MotionRecorder.js
|
|
```javascript
|
|
class MotionRecorder {
|
|
constructor() {
|
|
this.isRecording = false;
|
|
this.motionData = [];
|
|
this.startTime = null;
|
|
}
|
|
|
|
startRecording() {
|
|
this.isRecording = true;
|
|
this.motionData = [];
|
|
this.startTime = Date.now();
|
|
}
|
|
|
|
addFrame(motionFrame) {
|
|
if (this.isRecording) {
|
|
this.motionData.push({
|
|
...motionFrame,
|
|
relativeTime: Date.now() - this.startTime
|
|
});
|
|
}
|
|
}
|
|
|
|
stopRecording() {
|
|
this.isRecording = false;
|
|
return this.motionData;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. MotionExporter.js
|
|
```javascript
|
|
class MotionExporter {
|
|
exportToBVH(motionData) {
|
|
// BVH 포맷으로 변환
|
|
const bvhContent = this.generateBVHContent(motionData);
|
|
return this.createDownloadableFile(bvhContent, 'motion.bvh');
|
|
}
|
|
|
|
exportToFBX(motionData) {
|
|
// FBX 포맷으로 변환 (Three.js FBXExporter 활용)
|
|
const fbxContent = this.generateFBXContent(motionData);
|
|
return this.createDownloadableFile(fbxContent, 'motion.fbx');
|
|
}
|
|
|
|
exportToJSON(motionData) {
|
|
// JSON 포맷으로 내보내기
|
|
const jsonContent = JSON.stringify(motionData, null, 2);
|
|
return this.createDownloadableFile(jsonContent, 'motion.json');
|
|
}
|
|
|
|
createDownloadableFile(content, filename) {
|
|
const blob = new Blob([content], { type: 'application/octet-stream' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
link.click();
|
|
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🎨 사용자 인터페이스 사양
|
|
|
|
### HTML 구조
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>원격 모션캡쳐 시스템</title>
|
|
<link rel="stylesheet" href="css/style.css">
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- 연결 상태 -->
|
|
<div id="connection-status">
|
|
<span id="status-indicator"></span>
|
|
<span id="status-text">연결 대기 중...</span>
|
|
</div>
|
|
|
|
<!-- 3D 뷰어 -->
|
|
<div id="viewer-container">
|
|
<canvas id="three-canvas"></canvas>
|
|
</div>
|
|
|
|
<!-- 제어 패널 -->
|
|
<div id="control-panel">
|
|
<div id="recording-controls">
|
|
<button id="record-btn">⏺️ 녹화 시작</button>
|
|
<button id="stop-btn" disabled>⏹️ 정지</button>
|
|
<button id="play-btn" disabled>▶️ 재생</button>
|
|
</div>
|
|
|
|
<div id="recording-info">
|
|
<span id="timer">00:00:00</span>
|
|
<span id="frame-count">프레임: 0</span>
|
|
<span id="file-size">크기: 0KB</span>
|
|
</div>
|
|
|
|
<div id="export-controls">
|
|
<button id="export-bvh">BVH 다운로드</button>
|
|
<button id="export-fbx">FBX 다운로드</button>
|
|
<button id="export-json">JSON 다운로드</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 설정 패널 -->
|
|
<div id="settings-panel">
|
|
<h3>녹화 설정</h3>
|
|
<label>파일명: <input type="text" id="filename" value="motion_001"></label>
|
|
<label>프레임레이트:
|
|
<select id="framerate">
|
|
<option value="24">24 FPS</option>
|
|
<option value="30" selected>30 FPS</option>
|
|
<option value="60">60 FPS</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/websocketClient.js"></script>
|
|
<script src="js/motionViewer.js"></script>
|
|
<script src="js/motionRecorder.js"></script>
|
|
<script src="js/motionExporter.js"></script>
|
|
<script src="js/main.js"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### CSS 스타일 가이드
|
|
```css
|
|
/* 주요 색상 팔레트 */
|
|
:root {
|
|
--primary-color: #2c3e50;
|
|
--secondary-color: #3498db;
|
|
--success-color: #27ae60;
|
|
--warning-color: #f39c12;
|
|
--danger-color: #e74c3c;
|
|
--background-color: #ecf0f1;
|
|
}
|
|
|
|
/* 레이아웃 */
|
|
#app {
|
|
display: grid;
|
|
grid-template-areas:
|
|
"status status"
|
|
"viewer controls"
|
|
"viewer settings";
|
|
grid-template-columns: 2fr 1fr;
|
|
height: 100vh;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
#viewer-container {
|
|
grid-area: viewer;
|
|
background: #2c3e50;
|
|
border-radius: 8px;
|
|
position: relative;
|
|
}
|
|
|
|
/* 반응형 디자인 */
|
|
@media (max-width: 768px) {
|
|
#app {
|
|
grid-template-areas:
|
|
"status"
|
|
"viewer"
|
|
"controls"
|
|
"settings";
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
```
|
|
|
|
## ⚡ 성능 최적화
|
|
|
|
### 네트워크 최적화
|
|
- **데이터 압축**: gzip 압축 적용
|
|
- **전송 주기**: 30fps 기본, 네트워크 상태에 따라 조절
|
|
- **델타 압축**: 이전 프레임과의 차이만 전송
|
|
- **버퍼링**: 클라이언트 사이드 적응형 버퍼
|
|
|
|
### 렌더링 최적화
|
|
- **LOD 시스템**: 거리에 따른 모델 디테일 조절
|
|
- **프러스텀 컬링**: 카메라 시야 밖 객체 제외
|
|
- **인스턴싱**: 반복 객체 최적화
|
|
- **텍스처 압축**: 적절한 해상도 및 포맷 사용
|
|
|
|
### 메모리 최적화
|
|
- **오브젝트 풀링**: 반복 생성/삭제 객체 재사용
|
|
- **가비지 컬렉션**: 불필요한 객체 생성 최소화
|
|
- **IndexedDB**: 대용량 모션 데이터 효율적 저장
|
|
|
|
## 🔒 보안 고려사항
|
|
|
|
### 데이터 보안
|
|
- **HTTPS/WSS**: 암호화된 통신 프로토콜 사용
|
|
- **토큰 인증**: JWT 기반 사용자 인증
|
|
- **데이터 무결성**: 체크섬을 통한 데이터 검증
|
|
|
|
### 네트워크 보안
|
|
- **CORS 설정**: 허용된 도메인에서만 접근
|
|
- **레이트 리미팅**: 과도한 요청 방지
|
|
- **방화벽 설정**: 필요한 포트만 개방
|
|
|
|
## 📊 모니터링 및 로깅
|
|
|
|
### 성능 메트릭
|
|
- **지연시간**: WebSocket 왕복 시간 측정
|
|
- **프레임 드롭**: 누락된 프레임 수 추적
|
|
- **대역폭 사용량**: 실시간 네트워크 사용량
|
|
- **CPU/메모리 사용률**: 시스템 리소스 모니터링
|
|
|
|
### 로그 시스템
|
|
```javascript
|
|
class Logger {
|
|
static logMotionData(frameData) {
|
|
console.log(`[MOTION] Frame ${frameData.frameNumber} received`);
|
|
}
|
|
|
|
static logError(error, context) {
|
|
console.error(`[ERROR] ${context}:`, error);
|
|
}
|
|
|
|
static logPerformance(metric, value) {
|
|
console.log(`[PERF] ${metric}: ${value}ms`);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🧪 테스트 전략
|
|
|
|
### Unity 단위 테스트
|
|
- WebSocket 서버 연결/해제 테스트
|
|
- 모션 데이터 직렬화/역직렬화 테스트
|
|
- 녹화 기능 통합 테스트
|
|
|
|
### 웹 클라이언트 테스트
|
|
- WebSocket 통신 테스트
|
|
- 3D 렌더링 성능 테스트
|
|
- 파일 내보내기 기능 테스트
|
|
|
|
### 통합 테스트
|
|
- 전체 시스템 End-to-End 테스트
|
|
- 장시간 연결 안정성 테스트
|
|
- 다양한 네트워크 환경 테스트
|
|
|
|
---
|
|
|
|
*이 기술 사양서는 개발 과정에서 지속적으로 업데이트됩니다.*
|