JK강재신 / Unity · XR
← 프로젝트 목록

쓰리디아이 / 2020

조종사 비상탈출 낙하산 시뮬레이터

역재생 가능한 리플레이와 복수 센서 자세 결합

프레임 리플레이Kinect Merge팀 리딩
프로젝트 연도 / 기록 당시 상태2020
개발팀 · 리소스 인력 제외3인 · 팀장
사용 기술Unity3D · C# · VR · Winform

PROJECT IN ACTION

실행 화면과 시연

조종사 비상탈출 낙하산 시뮬레이터 프로젝트 화면
조종사 비상탈출 낙하산 시뮬레이터 · 시연 1 프로젝트 이미지
YouTube / PROJECT VIDEO

조종사 비상탈출 낙하산 시뮬레이터 · 시연 1

직접 열기 ↗

OVERVIEW

프로젝트와 나의 역할

훈련 리플레이, 상·하체 Kinect 데이터 결합, 프로젝트 고도화 및 팀 일정 관리를 담당했습니다.

  • 훈련 리플레이 기능 개발
  • 키넥트 자세 Merge 기능 개발
  • 프로젝트 고도화 작업 업무 분장 및 일정 관리

ENGINEERING NOTES

기능별 구현과 문제 해결

담당 기능의 배경부터 구현 방식과 선택한 범위까지 정리했습니다.

01

배속·역재생을 위한 기록 구조 변경

배경과 과제

훈련 State만 저장하면 배속이나 역재생 중 상태 전환과 애니메이션 제어에 문제가 생겼습니다.

내가 구현한 방식

위치, 낙하산 오브젝트의 활성 여부, 애니메이션 클립 상태를 리플레이용 데이터로 직접 기록하도록 변경했습니다.

선택의 이유와 범위

실시간 훈련 로직을 다시 실행하는 대신 재생에 필요한 상태를 기록하는 방향입니다. 훈련 데이터 전체가 아니라 재생에 필요한 정보를 선별했습니다.

2026 / RECONSTRUCTED EXAMPLE

시간축을 앞뒤로 탐색하는 리플레이

위치·회전은 보간하고, 활성 여부와 애니메이션 상태는 기록 시점에 맞춰 선택합니다.

  1. 표시 상태 기록
  2. 프레임 스냅샷
  3. 시간축 탐색
  4. 위치·상태 복원

설명용 개념도 · 실제 제품 화면이나 전체 시스템 설계도가 아닙니다.

사용 방법과 적용 범위

ReplayFrame 배열에 증가하는 시간을 넣고 Sample(frames, time)을 호출합니다. time을 줄이면 역방향 탐색이 됩니다.

단일 객체의 메모리 내 샘플입니다. 파일 입출력, 다중 객체 수명, 음성 동기화는 포함하지 않습니다.

예제 코드 펼치기포트폴리오용 재구성 코드 · 실제 업무 소스 아님
// 2026 portfolio reconstruction. Not original production source.
using System;
using UnityEngine;

namespace PortfolioExamples
{
    // A visual snapshot, deliberately independent from the live training state machine.
    public struct ReplayFrame
    {
        public float Time;
        public Vector3 Position;
        public Quaternion Rotation;
        public bool Visible;
        public int AnimationState;
        public float AnimationTime;
    }

    public static class ReplayTimeline
    {
        public static ReplayFrame Sample(ReplayFrame[] frames, float time)
        {
            if (frames == null || frames.Length == 0) throw new ArgumentException("No frames.");
            if (float.IsNaN(time) || float.IsInfinity(time)) throw new ArgumentException("Invalid time.");
            // This small standalone sample validates each call. A streaming adapter can validate once on import.
            for (int i = 0; i < frames.Length; i++)
            {
                if (float.IsNaN(frames[i].Time) || float.IsInfinity(frames[i].Time)) throw new ArgumentException("Invalid recorded time.");
                if (i > 0 && frames[i].Time <= frames[i - 1].Time) throw new ArgumentException("Times must increase.");
            }
            if (time <= frames[0].Time) return frames[0];
            if (time >= frames[frames.Length - 1].Time) return frames[frames.Length - 1];
            int lo = 0, hi = frames.Length - 1;
            while (hi - lo > 1) { int mid = (lo + hi) / 2; if (frames[mid].Time <= time) lo = mid; else hi = mid; }
            ReplayFrame a = frames[lo], b = frames[hi];
            float t = Mathf.InverseLerp(a.Time, b.Time, time);
            return new ReplayFrame {
                Time = time, Position = Vector3.Lerp(a.Position, b.Position, t),
                Rotation = Quaternion.Slerp(a.Rotation, b.Rotation, t),
                // Discrete values hold until the next recorded timestamp.
                Visible = a.Visible, AnimationState = a.AnimationState,
                AnimationTime = a.AnimationState == b.AnimationState
                    ? Mathf.Lerp(a.AnimationTime, b.AnimationTime, t) : a.AnimationTime
            };
        }
    }
}
02

Pelvis 기준으로 상·하체 자세 결합

배경과 과제

두 Kinect는 각자의 좌표계를 사용하므로 상체와 하체 데이터를 바로 합치기 어려웠습니다.

내가 구현한 방식

하체 Bone 데이터를 Pelvis 기준 로컬 좌표로 계산하고, 아바타 허리 기준으로 다시 적용해 상·하체를 결합했습니다.

선택의 이유와 범위

서로 다른 센서 좌표를 Pelvis라는 공통 신체 기준으로 옮기는 방식입니다. 아래 좌표 변환 예제와 실제 Kinect 자세 결합의 검증 범위는 구분됩니다.

2026 / RECONSTRUCTED EXAMPLE

공통 기준으로 자세 좌표 옮기기

센서 기준의 관절 자세를 로컬 좌표로 바꾼 뒤 아바타 기준으로 옮깁니다.

  1. 센서 관절
  2. 기준 Pose 역변환
  3. 아바타 기준 적용
  4. 출력 Pose

설명용 개념도 · 실제 제품 화면이나 전체 시스템 설계도가 아닙니다.

사용 방법과 적용 범위

보정된 sensorRoot, sensorJoint, avatarRoot Pose를 Retarget에 전달합니다.

축·단위·스케일은 미리 일치해야 합니다. 장비 캘리브레이션이나 스켈레톤 리깅 전체를 구현하지 않습니다.

예제 코드 펼치기포트폴리오용 재구성 코드 · 실제 업무 소스 아님
// 2026 portfolio reconstruction. Not original production source or a sensor SDK adapter.
using System;
using UnityEngine;
namespace PortfolioExamples
{
    public static class PoseAlignment
    {
        // Sensor and avatar roots must both be calibrated. Scaling is intentionally unsupported.
        public static Pose Retarget(Pose sensorRoot, Pose sensorJoint, Pose avatarRoot)
        {
            Vector3 localPosition = Quaternion.Inverse(sensorRoot.rotation) * (sensorJoint.position - sensorRoot.position);
            Quaternion localRotation = Quaternion.Inverse(sensorRoot.rotation) * sensorJoint.rotation;
            return new Pose(avatarRoot.position + avatarRoot.rotation * localPosition,
                avatarRoot.rotation * localRotation);
        }
        public static Pose AlignHead(Vector3 markerWorld, Quaternion hmdRotation, Quaternion rotationOffset)
            => new Pose(markerWorld, rotationOffset * hmdRotation);
    }
}
03

프로젝트 구조 이해와 정기 리팩터링

배경과 과제

큰 기존 프로젝트를 새 팀원이 빠르게 이해해야 했고, 작업이 늘면서 코드 배치도 일관되게 관리해야 했습니다.

내가 구현한 방식

초기 2주 동안 폴더 구조와 스크립트 관계를 설명하고, 다음 1주에는 팀원들이 구조를 분석해 서로 설명하도록 했습니다. SVN을 사용하며 2주 주기로 코드 위치와 구조를 함께 정리했습니다.

선택의 이유와 범위

코드를 대신 작성하는 방식보다 팀원이 구조를 설명하고 리뷰하는 방식으로 이해도를 확인했습니다.

MORE SCREENS

추가 실행 화면

CODE & VALIDATION

코드와 검증 범위

기존 코드 발췌와 2026년 포트폴리오용 재구성 코드를 구분해 제공합니다. 재구성 코드는 실제 제품 소스가 아니며, 일반 원리를 설명하기 위해 새로 작성했습니다.

Unity 6000.0.23f1 컴파일 및 Editor 검증 56개 통과. 실제 장비 연동과 제품 통합은 검증 범위에 포함하지 않습니다.

예제별 사용법과 검증 내용 ↗
다른 프로젝트 살펴보기 ↗