로딩 스크린과 인트로
강의 주제: Three.js 프로젝트에서 ShaderMaterial 기반의 오버레이와 HTML/CSS 로딩 바를 활용하여
에셋이 모두 로드될 때까지 자연스럽게 장면을 가리는 방법을 배웁니다.
1. 개요 및 목표
기본적으로 Three.js 앱은 WebGL 캔버스만 있고, 에셋이 로드되든 말든 곧바로 장면을 렌더링합니다. 이 레슨에서는:
- HTML과 CSS를 활용한 로딩 바 생성
- ShaderMaterial을 이용한 검은색 오버레이 화면 구현
- 모든 에셋이 로드된 후 자연스럽게 장면이 fade in되도록 구현
2. 오버레이 구현 (ShaderMaterial)
기본 오버레이 Plane 구성
const overlayGeometry = new THREE.PlaneGeometry(2, 2, 1, 1);
const overlayMaterial = new THREE.ShaderMaterial({
transparent: true,
uniforms: {
uAlpha: { value: 1.0 },
},
vertexShader: `
void main() {
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); //행렬적용
gl_Position = vec4(position, 1.0); // 행렬을 무시하여 전체 화면 채우기
}
`,
fragmentShader: `
uniform float uAlpha;
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha); // 투명도 조절
}
`,
});
const overlay = new THREE.Mesh(overlayGeometry, overlayMaterial);
scene.add(overlay);
PlaneGeometry(2, 2)는 화면 전체를 덮는 정사각형을 만듭니다.
ShaderMaterial로 작성했기 때문에 알파 블렌딩, fade 애니메이션이 가능합니다.
3. LoadingManager로 에셋 로드 상태 감지
const loadingManager = new THREE.LoadingManager(
// 모든 에셋 로딩 완료 시 실행
() => {
setTimeout(() => {
gsap.to(overlayMaterial.uniforms.uAlpha, {
duration: 3,
value: 0,
delay: 1,
});
loadingBarElement.classList.add('ended');
loadingBarElement.style.transform = '';
}, 500);
},
// 로딩 중 진행 상황
(itemUrl, itemsLoaded, itemsTotal) => {
const progressRatio = itemsLoaded / itemsTotal;
loadingBarElement.style.transform = `scaleX(${progressRatio})`;
},
);
LoadingManager는 GLTFLoader, CubeTextureLoader에 함께 전달되어 로딩 상황을 감지합니다.
4. HTML과 CSS 기반의 로딩 바
/src/index.html
<canvas class="webgl"></canvas>
<div class="loading-bar"></div>
/src/style.css
.loading-bar {
position: absolute;
top: 50%;
width: 100%;
height: 2px;
background: #ffffff;
transform: scaleX(0);
transform-origin: top left;
transition: transform 0.5s;
}
.loading-bar.ended {
transform: scaleX(0);
transform-origin: 100% 0;
transition: transform 1.5s ease-in-out;
}
.loading-bar.ended는 로딩 완료 후 오른쪽으로 사라지는 애니메이션을 위해 사용됩니다.
5. GSAP으로 오버레이 애니메이션
import { gsap } from 'gsap';
// 로딩 완료 후 오버레이를 자연스럽게 fade out
gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0, delay: 1 });
GSAP은 셰이더의 uniform 값에 애니메이션을 적용할 수 있어 활용도가 높습니다.
6. 로딩 테스트 팁 (Chrome)
네트워크 속도 시뮬레이션 방법
- DevTools > Network 탭 이동
- "Disable Cache" 체크
- Throttling > Add... > Custom profile 생성
- 예: "Fast 3G" 또는 사용자 정의 100000Kbps 등 설정
이 방법을 사용하면 로딩이 충분히 느려져서 로딩 애니메이션이 실제처럼 보임
7. 로딩 흐름 요약
- HTML/CSS로 로딩 바 구성 (transform: scaleX)
- Three.js LoadingManager로 진행률 계산
- 진행률에 따라 .style.transform = scaleX(progress) 적용
- 에셋 로딩 완료 후:
- .loading-bar.ended 클래스 적용 (오른쪽으로 사라짐)
- ShaderMaterial.uAlpha를 GSAP으로 0으로 애니메이션 (검은 오버레이 사라짐)
✨ 마무리 요약
- 오버레이는 ShaderMaterial + PlaneGeometry 조합으로 구현
- 로딩 바는 HTML/CSS + JavaScript DOM 조작으로 구현
- 모든 에셋 로드 감지는 LoadingManager로 처리
- GSAP으로 fade-out 트랜지션 구현
이 방법을 응용하면 Three.js 프로젝트에서 훨씬 더 부드럽고 고급스러운 UX의 인트로 효과를 줄 수 있습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Intro and loading progress</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<canvas class="webgl"></canvas>
<div class="loading-bar"></div>
<script type="module" src="./script.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
}
html,
body {
overflow: hidden;
}
.webgl {
position: fixed;
top: 0;
left: 0;
outline: none;
}
.loading-bar {
position: absolute;
top: 50%;
width: 100%;
height: 2px;
background: #fff;
transform: scaleX(0);
transform-origin: top left;
transition: transform 0.5s;
will-change: transform;
}
.loading-bar.ended {
transform: scaleX(0);
transform-origin: top right;
transition: transform 1.5s ease-in-out;
}
import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { gsap } from 'gsap'; /** * Loaders */ const loadingBarElement = document.querySelector('.loading-bar'); const loadingManager = new THREE.LoadingManager( // Loaded () => { gsap.delayedCall(0.5, () => { gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0 }); // 상대적으로 가벼운 gsap 사용 loadingBarElement.classList.add('ended'); loadingBarElement.style.transform = ``; // js가 css보다 나중에 적용되므로 progress transform 무효화 }); // css 애니메이션을 조금 더 보여줌 window.setTimeout(() => {}, 500);과 동일 }, // Progress (itemUrl, itemsLoaded, itemsTotal) => { const progressRatio = itemsLoaded / itemsTotal; loadingBarElement.style.transform = `scaleX(${progressRatio})`; }, ); const gltfLoader = new GLTFLoader(loadingManager); const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager); /** * Base */ // Debug const debugObject = {}; // Canvas const canvas = document.querySelector('canvas.webgl'); // Scene const scene = new THREE.Scene(); /** * Overlay */ const overlayGeometry = new THREE.PlaneGeometry(2, 2, 1, 1); const overlayMaterial = new THREE.ShaderMaterial({ transparent: true, uniforms: { uAlpha: { value: 1 }, }, vertexShader: ` void main() { // gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform float uAlpha; void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha); } `, }); const overlay = new THREE.Mesh(overlayGeometry, overlayMaterial); scene.add(overlay); /** * Update all materials */ const updateAllMaterials = () => { scene.traverse((child) => { if ( child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial ) { // child.material.envMap = environmentMap child.material.envMapIntensity = debugObject.envMapIntensity; child.material.needsUpdate = true; child.castShadow = true; child.receiveShadow = true; } }); }; /** * Environment map */ const environmentMap = cubeTextureLoader.load([ '/textures/environmentMaps/0/px.jpg', '/textures/environmentMaps/0/nx.jpg', '/textures/environmentMaps/0/py.jpg', '/textures/environmentMaps/0/ny.jpg', '/textures/environmentMaps/0/pz.jpg', '/textures/environmentMaps/0/nz.jpg', ]); environmentMap.colorSpace = THREE.SRGBColorSpace; scene.background = environmentMap; scene.environment = environmentMap; debugObject.envMapIntensity = 2.5; /** * Models */ gltfLoader.load('/models/FlightHelmet/glTF/FlightHelmet.gltf', (gltf) => { gltf.scene.scale.set(10, 10, 10); gltf.scene.position.set(0, -4, 0); gltf.scene.rotation.y = Math.PI * 0.5; scene.add(gltf.scene); updateAllMaterials(); }); /** * Lights */ const directionalLight = new THREE.DirectionalLight('#ffffff', 3); directionalLight.castShadow = true; directionalLight.shadow.camera.far = 15; directionalLight.shadow.mapSize.set(1024, 1024); directionalLight.shadow.normalBias = 0.05; directionalLight.position.set(0.25, 3, -2.25); scene.add(directionalLight); /** * Sizes */ const sizes = { width: window.innerWidth, height: window.innerHeight, }; window.addEventListener('resize', () => { // Update sizes sizes.width = window.innerWidth; sizes.height = window.innerHeight; // Update camera camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix(); // Update renderer renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); }); /** * Camera */ // Base camera const camera = new THREE.PerspectiveCamera( 75, sizes.width / sizes.height, 0.1, 100, ); camera.position.set(4, 1, -4); scene.add(camera); // Controls const controls = new OrbitControls(camera, canvas); controls.enableDamping = true; /** * Renderer */ const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, }); renderer.toneMapping = THREE.ReinhardToneMapping; renderer.toneMappingExposure = 3; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); /** * Animate */ const tick = () => { // Update controls controls.update(); // Render renderer.render(scene, camera); // Call tick again on the next frame window.requestAnimationFrame(tick); }; tick();'Graphic > ThreeJS' 카테고리의 다른 글
| Model Lisence CC0 (4) | 2025.08.18 |
|---|---|
| 48 3D 위치에 HTML 요소 고정하기 (HTML과 WebGL 혼합) (3) | 2025.08.07 |
| 46 퍼포먼스 최적화 (6) | 2025.08.06 |
| 45 후처리(Post-Processing) (5) | 2025.08.06 |
| 31 MeshStandardMaterial 셰이더 확장(Modified materials) (2) | 2025.08.05 |