Three.js로 360˚ 비디오 플레이어 구현하기

Created at 2025년 06월 05일

Updated at 2025년 06월 05일

By 강병준

개요

서비스에서 제공하는 영상에서 360˚ 비디오 플레이어 기능이 추가되어야 하는 상황이 생겨 3D 렌더링 기술이 필요하게 되었다. 이러한 3D 렌더링을 웹에서 구현하기 위해서는 크게 Three.js, Babylon.js 등의 기술이 존재한다.

Babylon.js는 완전한 3D 엔진으로 물리 엔진, 애니메이션 GUI 등의 필요한 대부분의 기능을 제공한다는 큰 장점이 있지만, 반대로 3D 엔진에 대한 깊은 이해도가 요구된다는 높은 러닝 커브가 존재한다. 반면 Three.js는 경량화된 3D 엔진 라이브러리로 BabyIon에 비해 WebXR 지원이나 세밀한 3D 엔진 제어 기능은 부족하지만, Three.js의 Fundamentals을 엿보면 알 수 있듯이 매우 직관적인 방식과 입문자를 위한 충분한 문서를 제공하고 있다.

또한 가장 중요한 우리 서비스에서 필요로 하는 인터랙티브와 3D 렌더링 기능을 처리할 수 있어 가장 보편적이며 빠르게 적용할 수 있는 Three.js를 선택하게 되었다.

이 글에서는 Three.js의 기본 설정부터 영상 텍스처 적용, 카메라 컨트롤 구현까지 핵심 코드와 팁을 하나씩 다뤄보겠다.

Three.js

💡 WebGL (Web Graphics Library)

MDN문서에 따르면 WebGL이란 웹 브라우저에서 고성능 인터랙티브 3D 및 2D 그래픽을 렌더링하는 JavaScript API라고 나와있다. 단순하게 생각하면 WebGL은 점, 선, 삼각형을 그리는 아주 단순한 시스템이라고 생각해도 될 것 같다.

Three.js는 WebGL 기반으로 동작하며, 웹페이지에서 3D 객체를 쉽게 렌더링할 수 있도록 도와주는 JavaScript 라이브러리다. WebGL을 이용하여 3차원 세계를 구현하게 되는데,

  • 씬 (Scene)
  • 광원
  • 그림자
  • 물체
  • 텍스처 등

3차원 세계를 하나씩 하나씩 디테일하게 구현한다면 굉장히 복잡해질 것이다. 그런데 Three.js는 이런 3D 요소들의 처리를 도와 직관적인 코드를 쉽고 빠르게 짤 수 있도록 도와준다.

자! 이제 3D 엔진에서 사용되는 기본적인 개념을 간단하게 살펴보자.

3D 엔진 기본 구성 요소

three.js를 사용해서 어떤 물체를 나타내기 위해서는 기본적으로 세 가지가 필요하다.

  1. Scene
  2. Camera
  3. Renderer

image.png

Scene은 가상의 3차원 공간(정사각형)이라고 생각할 수 있다. 여기서 우리는 가상의 3차원 공간에 3D 객체와 Camera를 배치하고, Camera로 촬영된 장면을 Renderer를 통해서 우리 화면에 표시한다.

그리고 카메라의 종류에는 두 가지가 있다.

  1. Perspective Camera
  2. Orthographic Camera

Perspective Camera는 현실 세계의 시각과 유사하게 원근감을 표현하는 카메라로, 가까운 물체는 크게, 멀리 있는 물체는 작게 보이도록 3D 뷰를 제공하는데 사용되는 카메라이고, Orthographic Camera는 거리와 무관하게 객체의 크기가 변하지 않게 보이는 카메라이다.

각각 아래의 상황에서 사용된다고 보면 편할 것 같다.

  1. Perspective Camera - 3D 객체 뷰
  2. Orthographic Camera - 2D ui, 지도, 다이어그램 등

image.png

image.png

출처: Bepro Team Blog - 자바스크립트로 360˚ 비디오 플레이어 구현하기

이제 Three.js를 이용하여 360˚ 비디오 플레이어를 구현할 것이다. 구현은 아래 단계로 이루어진다:

  1. 기본 구조 잡기

    • Scene 생성
    • Camera 생성
    • WebGL Renderer 생성
  2. 영상 스트리밍 객체 생성

    • 비디오 객체 생성
    • HLS 비디오 스트리밍 재생
  3. 3D 객체 생성

    • 비디오 텍스처 생성
    • 비디오 텍스처를 입힐 재질 생성
    • 3D 메시 생성 및 위치 지정
  4. 애니메이션 루프 실행

360˚ 비디오 플레이어 구현

1. 기본 구조 잡기

// 1-1. Scene 생성
const scene = new THREE.Scene();

우선 가상의 3차원 공간을 정의한다.

// 1-2. Camera 생성
const fov = 75; // 시야각
const aspect = 640 / 480; // 종횡비 (width / height)
const near = 1; // 카메라에서 가까운 면
const far = 100; // 카메라에서 가장 먼 면
 
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

영상을 3D로 렌더링하기 위해 여기서는 Perspective Camera를 이용하여 생성한다. 그리고 카메라를 생성할 때 전달하는 매개변수는 다음과 같다.

image.png

출처: three.js Cameras

  • fov (시야각)

카메라에서 얼마나 넓은 범위를 볼 것인지를 정의하는 시야각으로, 일반적으로 인간의 중심 시야에 가까운 60 ~ 75˚로 설정한다고 한다. 하지만 서비스마다 적절한 시야각을 조절해서 사용하면 된다.

다만 시야각의 경우에는 너무 크면 광각 렌즈처럼 왜곡되고, 적으면 망원렌즈처럼 보일 수 있기 때문에 적절한 값을 선택해주는 것이 중요하다.

  • aspect (종횡비)

시야의 가로/세로 비율로 브라우저 창의 비율에 맞게 가로폭을 계산하는 데 사용한다. 3D 객체가 렌더링되는 컨테이너의 크기에 맞게 조절하면 된다.

  • near (카메라에서 가까운 면)

카메라에서 가까운 면으로 이 거리보다 가까운 객체는 렌더링되지 않는다.

  • far (카메라에서 가장 먼 면)

카메라에서 가장 먼 면으로 이 거리보다 먼 객체는 렌더링되지 않는다.

// 1-3. WebGL Renderer 생성
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);

Scene과 Camera를 조합해서 실제 우리 PC 화면에 그려주기 위해 renderer를 생성하고, 렌더링할 크기를 지정한다.

여기서 WebGLRenderer를 생성할 때 antialias 속성을 전달하는 것을 확인할 수 있는데 해당 옵션은 계단 현상을 줄이고 부드러운 화면을 제공하기 위한 옵션이다.

image.png

코드: https://github.com/BangDori/blog-code/tree/main/threejs-test/basic-setup

기본 설정을 끝마치고나면 위 화면과 마찬가지로 검은색 사각형을 확인할 수 있다. 하지만 아무런 3D 객체를 살펴볼 수 없는 것을 확인할 수 있는데, 이는 가상 세계와 카메라만을 설정해준 상태로 화면에 렌더링을 진행해서 그렇다.

이제 다음 단계로 360도 비디오 플레이어 구현을 위해 영상 스트리밍 객체를 생성해보자.

2. 영상 스트리밍 객체 생성

// 2-1 비디오 객체 생성
const video = document.createElement("video");
video.crossOrigin = "anonymous";
video.playsInline = true;
video.volume = 1.0;
video.muted = true;

영상을 재생하기 위해서 위와 같이 비디오 태그를 생성한다. 여기서 사용되는 각 옵션은 아래와 같다.

  1. crossOrigin: CORS 정책에 따라 동작 방식을 지정해주는 속성
  2. playsInline: 인라인에서 비디오가 재생되도록 하는 옵션
  3. volume: 비디오 음량 (0.0 = 무음, 1.0 = 최대 음량)
  4. muted: 비디오를 무음 상태로 설정합니다.

muted 속성을 무음 모드(true)로 설정한 이유는 Chrome, Safari 등에서 autoplay를 허용하기 위해서는 Autoplay policy in Chrome에 따라 무음 모드로 설정해야하기 때문이다.

// 2-2. HLS 비디오 스트리밍 재생
const videoUrl = "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8";
 
if (Hls.isSupported()) {
  // hls.js를 사용한 재생
  const hls = new Hls();
  hls.loadSource(videoUrl); // m3u8 URL 로드
  hls.attachMedia(video); //  HLS.js에서 만든 스트림을 video에 연결
  hls.on(Hls.Events.MANIFEST_PARSED, () => video.play()); // 파싱 완료 시 재생 시작
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
  // Safari 등 브라우저 기본 지원을 이용한 재생
  video.src = videoUrl;ㄴ
  video.play();
}

💡 HLS (HTTP Live Streaming)

HLS는 가장 널리 사용되는 비디오 스트리밍 프로토콜로, 포맷 형식이 아니라 비디오 파일을 작은 조각(.ts 파일)으로 나누고, 이 조각들의 재생 목록(.m3u8 파일)을 통해 HTTP로 순차적으로 전송하는 방식입니다.

브라우저에서 제공하는 표준 지원 형식(mp4, mov)이 아닌 HLS 스트리밍 프로토콜을 이용하여 영상을 불러오기 때문에 위와 같이 Hls.js 라이브러리를 적용해야한다.

3. 3D 객체 생성

// 3-1. 원통 생성
const radius = 300;
const height = (2 * radius) / (16 / 9); // 16:9 비율
const radialSegments = 60;
const heightSegments = 1;
const openEnded = true;
 
const cylinderGeometry = new THREE.CylinderGeometry(
  radius, // 위쪽 반지름
  radius, // 아래쪽 반지름 (같으면 원기둥)
  height, // 실린더 높이 (y 방향)
  radialSegments, // 둘레 분할 수 → 더 클수록 텍스처 퀄리티 부드러움
  heightSegments, // 높이 방향 분할 수 → 보통 1이면 충분
  openEnded, // 뚜껑 없음 → 실린더 안에서 텍스처(영상) 보기
);

360˚ 영상을 표현하기 위해 CylinderGeometry를 적용하였다. 보통 360˚ 영상에서는 SphereGeometry(구체)를 사용하지만, 우리 서비스에서는 구체 방식보다는 파노라마 영상처럼 제공해주기 위해 CylinderGeometry(원통형)를 이용하였다.

CylinderGeometry 객체를 생성하기 위해 사용된 옵션은 순서대로 다음과 같다.

  • radiusTop - 원통의 위쪽 반지름 (기본 값 = 1)
  • radiusBottom - 원통의 아래쪽 반지름, radiusTop과 동일한 경우 원기둥 (기본 값 = 1)
  • cylinderHeight - 실린더의 높이 y축 (기본 값 = 1)
  • radialSegments - 둘레 분할 수로 값이 커질수록 텍스처가 부드러워 짐 (기본 값 = 32)
  • heightSegments - 높이 방향 분할 수 (기본 값 = 1)
  • openEnded - 뚜껑 유무를 나타내는 값 (기본 값 = false)
// 3-2. 비디오 텍스처 생성
const texture = new THREE.VideoTexture(video);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;

그리고 이제 HTML video 요소를 기반으로 3D 오브젝트에 입힐 비디오 텍스처를 생성한다.

비디오를 바로 재생하지 않고, 3D 기반으로 만드는 이유는 우리가 영상을 360˚ 움직이며 줌을 처리해야하기 때문에 텍스처를 생성해주어야한다.

maxFilter, magFilter는 각각 텍스처가 축소되어 있을 때와 확대되어 있을 때 어떻게 보간할지를 설정하는 것으로 선형 보간(LinearFilter)을 적용하여 픽셀 사이를 부드럽게 보이도록 했다.

// 3-3. 비디오 텍스처를 입힐 재질 생성
const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide });

비디오 텍스처를 3D 오브젝트에 입히기 위해 MeshBasicMaterial을 사용하였다. 이 재질은 조명이나 그림자의 영향을 받지 않으며, 단순히 텍스처만을 화면에 표시하는 데 적합하다.

매개 변수로 전달한 각 속성의 역할은 다음과 같다:

  • `map: texture``:앞서 생성한 비디오 텍스처를 표면에 입히는 속성
  • side: THREE.BackSide: 객체의 안쪽 면에만 텍스처를 표시하도록 설정
    • 이 설정은 360˚ 영상이나 VR 영상을 감상할 때, 외부를 바라보는 것이 아니라 내부를 바라보도록 해야하기 때문에 설정한 옵션이다.
    • 만약 일반적인 외부 시점에서 영상을 입힌다면 THREE.FrontSide을 사용하면 된다.
// 3-4. 3D 메시 생성 및 위치 지정
const mesh = new THREE.Mesh(cylinderGeometry, material);
scene.add(mesh);

여기서 Mesh는 실제 3D 씬에 렌더링되는 객체로 geometry(형태)와 material(표면 재질)을 합쳐서 생성하고, 3D 객체를 Scene에 추가한다.

4. 애니메이션 루프 실행

이제 최종적으로 애니메이션을 실행시켜주기만 하면 완성된다.

// 3D 장면을 실시간으로 그리는 애니메이션 루프
renderer.setAnimationLoop(() => renderer.render(scene, camera));

이 코드는 Three.js에서 권장하는 방식으로, 실시간으로 3D 장면을 렌더링하기 위한 애니메이션 루프를 구성한다.

  • renderer.setAnimationLoop()는 WebXR(AR/VR) 환경에서도 호환되며, 일반적인 브라우저 환경에서도 프레임마다 지정한 콜백 함수를 호출해준다.
  • 내부적으로 requestAnimationFrame과 유사하게 동작하지만, WebXR 지원이 필요한 경우에도 자동으로 최적화된 루프를 제공한다.
  • 콜백 함수 안에서 renderer.render(scene, camera)를 호출하여, 현재 카메라 시점에서 씬(scene)을 매 프레임마다 렌더링한다.

이 방식 덕분에 사용자가 보는 화면이 실시간으로 부드럽게 움직이고, 비디오 텍스처도 실제 공간 안에 있는 것처럼 자연스럽게 재생된다. 특히 WebXR(AR/VR) 환경까지 고려한다면, setAnimationLoop 사용이 더욱 권장된다.

자, 이제 이렇게 하면 최종 결과물인 360˚ 영상 플레이어가 완료된다!

예시 코드는 아래 레포지토리를 확인하면 된다!!

참고