본문 바로가기
Graphic/R3F

64 Laptop Scene

by curious week 2025. 8. 18.

Portfolio with Laptop Scene

이번 레슨에서는 HTML/CSS로 만든 포트폴리오 사이트를 3D 노트북 화면 속에 넣는 방식을 배웁니다.
이 방법은 단순히 3D 모델을 보여주는 것이 아니라, 자신의 웹사이트를 WebGL 장면 속에서 인터랙티브하게 시각화할 수 있는 강력한 포트폴리오 아이디어입니다.


🎯 핵심 목표

  1. 배경을 어둡게 설정 → 콘텐츠 강조
  2. 노트북 3D 모델 불러오기 (useGLTF)
  3. 환경광 + 그림자 + 애니메이션으로 사실감 추가
  4. PresentationControls로 카메라 대신 모델 제어
  5. <Html> + <iframe>으로 실제 웹사이트를 화면에 넣기
  6. 조명(rectAreaLight)과 텍스트(Text)로 디테일 강화

1. Setup

// Experience.jsx
import {
  useGLTF,            // GLTF 모델 로딩 훅
  Environment,        // HDRI 환경맵 (빛/반사/배경)
  Float,              // 부유 애니메이션 래퍼
  PresentationControls, // 드래그/회전 제어 (orbit-like)
  ContactShadows,     // 평면 그림자
  Html,               // 3D 공간에 HTML 고정
  Text,               // 3D 텍스트
} from '@react-three/drei';

export default function Experience() {
  // useGLTF(url: string) → { scene, nodes, materials }
  // - glTF/GLB 모델을 비동기로 로딩
  const computer = useGLTF(
    'https://vazxmixjsiawhamofees.supabase.co/.../macbook/model.gltf',
  );

  return (
    <>
      {/* Scene 배경색 
          - <color attach="background" /> → 씬의 배경을 특정 색으로 지정
          - args: [r,g,b] 또는 hex string */}
      <color args={['#241a1a']} attach="background" />

      {/* Environment (HDRI 환경맵)
          - preset: "city", "sunset", "night", "dawn", "warehouse", "forest" 등 미리 정의된 HDRI */}
      <Environment preset="city" />

      {/* PresentationControls 
          - 모델을 드래그로 회전/줌 할 수 있게 하는 컨트롤
          props:
          - global: boolean → 씬 전체에 적용
          - rotation: [x,y,z] 초기 회전값 (rad)
          - polar: [min,max] 세로 회전 제한 (위/아래)
          - azimuth: [min,max] 가로 회전 제한 (좌/우)
          - config: spring 물리 효과 { mass, tension, friction }
          - snap: boolean → 놓았을 때 스냅(원위치) */}
      <PresentationControls
        global
        rotation={[0.13, 0.1, 0]}
        polar={[-0.4, 0.2]} // 위로 0.2rad, 아래로 -0.4rad 까지 제한
        azimuth={[-1, 0.75]} // 왼쪽 -1rad, 오른쪽 0.75rad 제한
        config={{ mass: 2, tension: 400 }}
        snap
      >
        {/* Float 
            - children을 천천히 부유(floating)하는 애니메이션
            props:
            - rotationIntensity: number (기본 1) → 얼마나 회전하며 부유할지
            - floatIntensity: number → 상하 이동 강도
            - speed: number → 움직임 속도 */}
        <Float rotationIntensity={0.4}>
          {/* rectAreaLight
              - 사각형 모양의 면광 (네온사인, 모니터광 표현에 유용)
              props:
              - width, height: number → 빛의 면적
              - intensity: number → 광원 세기
              - color: string | THREE.Color
              - rotation: [x,y,z] 라디안 회전값
              - position: [x,y,z] 위치 */}
          <rectAreaLight
            width={2.5}
            height={1.65}
            intensity={65}
            color={'#ff6900'}
            rotation={[-0.1, Math.PI, 0]}
            position={[0, 0.55, -1.15]}
          />

          {/* GLTF 모델 삽입
              - <primitive object={computer.scene} /> 
              - object: THREE.Object3D → GLTF의 루트 오브젝트
              - position-y={-1.2}: 모델을 y축 아래로 내림 */}
          <primitive object={computer.scene} position-y={-1.2}>
            {/* Html 
                - 3D 오브젝트 위에 실제 DOM 요소 삽입 (iframe, div 등)
                props:
                - transform: boolean → 3D 변환을 적용 (크기/회전)
                - wrapperClass: string → CSS className
                - distanceFactor: number → 카메라 거리 기반 scale 보정
                - position: [x,y,z] → 3D 위치
                - rotation-x: number → x축 회전 */}
            <Html
              transform
              wrapperClass="htmlScreen"
              distanceFactor={1.17}
              position={[0, 1.56, -1.4]}
              rotation-x={-0.256}
            >
              {/* Html 내부에는 일반 HTML 요소 가능 */}
              <iframe src="https://bruno-simon.com/html/" />
            </Html>
          </primitive>

          {/* Text 
              - drei에서 제공하는 텍스트 컴포넌트
              props:
              - font: string → 폰트 파일 경로 (.woff, .ttf)
              - fontSize: number → 글자 크기
              - position: [x,y,z] → 위치
              - rotation-y: number → y축 회전
              - maxWidth: number → 최대 줄바꿈 폭 */}
          <Text
            font="./bangers-v20-latin-regular.woff"
            fontSize={1}
            position={[2, 0.75, 0.75]}
            rotation-y={-1.25}
            maxWidth={2}
          >
            BRUNO SIMON
          </Text>
        </Float>
      </PresentationControls>

      {/* ContactShadows 
          - 바닥에 "soft shadow" 생성
          props:
          - position-y: number → 그림자 평면의 높이
          - opacity: number → 그림자 투명도
          - scale: number

2. CSS 설정

/* iframe 스타일링 */
.htmlScreen iframe {
  width: 1024px;
  height: 670px;
  border: none;
  border-radius: 20px; /* 약간의 곡선 처리 */
  background: #000; /* 로딩 전 대비 */
}

/* 모바일 제스처 충돌 방지 */
.r3f {
  touch-action: none;
}

3. 주요 개념 정리

🔹 PresentationControls

  • global: 화면 전체에서 드래그 가능
  • polar: 세로 회전 제한 [min, max]
  • azimuth: 가로 회전 제한 [min, max]
  • damping: 애니메이션 반응 속도
  • snap: 원위치로 돌아오는 효과

🔹 <Html> (drei)

  • 3D 오브젝트에 HTML 요소를 붙일 수 있는 헬퍼
  • transform: 실제 3D 좌표/회전을 따르도록 변환
  • wrapperClass: CSS 적용을 위한 클래스
  • distanceFactor: 크기 조정 (가까운/먼 거리 보정)

🔹 ContactShadows

  • 바닥에 간단하고 성능 좋은 그림자 표현
  • opacity, scale, blur로 조절 가능

🔹 rectAreaLight

  • 사각형 면광 → 화면 발광 효과 표현
  • width, height로 크기 조정
  • intensity, color로 빛 성질 조절

🔹 Text (drei)

  • 3D 텍스트 렌더링
  • font, fontSize, maxWidth, textAlign 지원
  • .woff 같은 웹폰트 사용 가능

4. UX 개선 아이디어

  • 로딩 후 객체가 등장하는 인트로 애니메이션
  • 마우스 hover 시 카메라 줌인
  • iframe 내 CSS 개선 및 모바일 대응
  • 반사 표현, 사운드, 배경 오브젝트, 파티클 추가
  • Easter egg 요소 삽입

요약

  • 노트북 모델 + Html iframe 조합으로 웹사이트를 실물처럼 3D에 삽입
  • PresentationControls + Float로 자연스러운 조작과 애니메이션 구현
  • 조명, 그림자, 텍스트로 사실감과 브랜딩 강화
  • CSS + distanceFactor로 iframe의 크기와 품질 최적화

// Experience
import {
  Html,
  ContactShadows,
  PresentationControls,
  Environment,
  Float,
  useGLTF,
  Text,
} from '@react-three/drei';

export default function Experience() {
  const computer = useGLTF(
    'https://threejs-journey.com/resources/models/macbook_model.gltf',
  );

  return (
    <>
      <Environment preset="city" />

      <color args={['#241a1a']} attach="background" />

      {/* PresentationControls 카메라 대신 모델을 조작 */}
      <PresentationControls
        global
        rotation={[0.13, 0.1, 0]}
        polar={[-0.4, 0.2]} // 세로 제한
        azimuth={[-1, 0.75]} // 가로 제한
        damping={0.1} // 움직임 속도
        config={{ mass: 2, tension: 400 }}
        snap>
        <Float rotationIntensity={0.4}>
          {/* Light */}
          <rectAreaLight
            width={2.5}
            height={1.65}
            intensity={65}
            color={'#ff6900'}
            rotation={[0.1, Math.PI, 0]}
            position={[0, 0.55, -1.15]}
          />
          {/* Model */}
          <primitive object={computer.scene} position-y={-1.2}>
            <Html
              wrapperClass="htmlScreen"
              transform
              distanceFactor={1.17}
              position={[0, 1.56, -1.4]}
              rotation-x={-0.256}>
              <iframe src="https://bruno-simon.com/html" />
            </Html>
          </primitive>

          {/* Text */}
          <Text
            font="./bangers-v20-latin-regular.woff"
            fontSize={1}
            position={[2, 0.75, 0.75]}
            rotation-y={-1.25}
            // children={'iframe\nTEST'}
            maxWidth={2}
            textAlign="center">
            Model+ Iframe
          </Text>
        </Float>
      </PresentationControls>

      <ContactShadows position-y={-1.4} opacity={0.4} scale={5} blur={2.4} />
    </>
  );
}
/* CSS */
html,
body,
#root {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: ivory;
}

/* 모바일에서 제스처 충돌 막기 (슬라이드 시 리로딩 같은)*/
.r3f {
  touch-action: none;
}

.htmlScreen iframe {
  /* 내부 스크린 해상도 확보 width, height */
  width: 1024px;
  height: 670px;
  border: none;
  border-radius: 20px;
  background: #000;
}

'Graphic > R3F' 카테고리의 다른 글

66 Mini-Game: Marble Race  (2) 2025.08.20
65 React Three Rapier  (13) 2025.08.19
63 후처리(Post Processing)  (5) 2025.08.18
62 Pointer Events (Mouse Events)  (7) 2025.08.18
61 Portal Scene  (0) 2025.08.18