이미지 슬라이더
이 강의에서는 텍스처 이미지를 로드하고 셰이더에서 사용하는 방법을 배워, 이 반응형 이미지 슬라이더를 만들어 보겠습니다:
여기 모바일 버전 최종 결과입니다:
이 프로젝트는 Sikriti Dakua님의 Codepen에서 영감을 받았습니다.
이 효과를 만드는 방법을 배우고 싶다는 동기 부여가 되었길 바랍니다. 시작해 봅시다!
스타터 프로젝트
스타터 프로젝트는 로고, 메뉴 버튼, 그리고 장면 중앙에 하얀 큐브가 있는 <Canvas>
컴포넌트를 포함한 전체 화면 섹션이 있습니다.
우리는 HTML 요소를 애니메이션하기 위해 Framer Motion를 사용할 것입니다. 다른 라이브러리나 일반 CSS를 사용하여 애니메이션할 수도 있습니다. Framer Motion의 기본 버전만 사용할 것이며, 3D 패키지를 설치할 필요는 없습니다.
UI를 위해 Tailwind CSS를 선택했지만, 편한 솔루션을 사용하셔도 됩니다.
public/textures/optimized
폴더에는 슬라이더에서 사용할 이미지들이 있습니다. 이 이미지는 Leonardo.Ai로 AI 생성 후 Squoosh를 사용하여 최적화했습니다. 3:4 비율을 선택하여 모바일에서 보기 좋게 세로 방향으로 만들었습니다.
Squoosh로 3.9MB에서 311KB로 최적화된 이미지 중 하나입니다.
이미지 슬라이더 컴포넌트
흰색 큐브를 이미지 표시용 평면으로 교체해보겠습니다. ImageSlider
라는 새 컴포넌트를 생성합니다:
export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { return ( <mesh> <planeGeometry args={[width, height]} /> <meshBasicMaterial color="white" /> </mesh> ); };
사용할 이미지의 종횡비에 맞게 width와 height를 조정하세요.
fillPercent
prop는 화면 높이/너비의 일정 비율만큼만 평면 크기를 조정하는 데 사용됩니다.
App.jsx
에서 ImageSlider
컴포넌트를 임포트하여 흰색 큐브를 그것으로 대체합니다:
import { ImageSlider } from "./ImageSlider"; // ... function App() { return ( <> {/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> {/* ... */} </> ); } // ...
결과는 다음과 같습니다:
평면이 너무 많은 공간을 차지하고 있습니다
평면이 반응형이 되어 화면 높이의 75%(fillPercent
)만 차지하도록 만들고 싶습니다. 이를 위해 useThree
훅을 사용하여 viewport
의 크기를 가져오고, 평면 크기를 조정할 스케일 팩터를 만듭니다:
import { useThree } from "@react-three/fiber"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const viewport = useThree((state) => state.viewport); const ratio = viewport.height / (height / fillPercent); return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <meshBasicMaterial color="white" /> </mesh> ); };
스케일 팩터를 계산하기 위해 viewport.height
를 평면의 height
나누기 fillPercent
으로 나눕니다. 이렇게 하면 평면을 스케일링할 수 있는 비율을 얻을 수 있습니다.
이 수학적 계산을 이해하기 위해,
viewport.height
를 평면의 최대 높이로 생각할 수 있습니다. 뷰포트 높이가 3이고 평면 높이가 4라면, 평면이 화면에 맞도록3 / 4
로 스케일링해야 합니다. 그러나 화면 높이의 75%만 차지하고 싶기 때문에 평면 높이를fillPercent
로 나눠 새로운 기준 높이를 얻습니다. 즉,4 / 0.75 = 5.3333
가 됩니다.
그런 다음 width
와 height
에 ratio
를 곱하여 새로운 크기를 얻습니다.
수직으로 크기를 조절할 때는 잘 작동하지만 수평으로는 그렇지 않습니다. 화면 높이가 너비보다 큰 경우 평면 너비를 화면 너비의 75%만 차지하도록 조정해야 합니다.
import { useThree } from "@react-three/fiber"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const viewport = useThree((state) => state.viewport); let ratio = viewport.height / (height / fillPercent); if (viewport.width < viewport.height) { ratio = viewport.width / (width / fillPercent); } return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <meshBasicMaterial color="white" /> </mesh> ); };
const
에서 let
으로 ratio
를 변경하여 재할당할 수 있게 설정하는 것을 잊지 마십시오. (또는 삼항 연산자를 사용하는 방법도 있습니다)
이제 평면이 반응형이 되어 화면 크기에 따라 화면 높이 또는 너비의 75%만 차지합니다.
이제 평면에 이미지를 표시할 준비가 되었습니다.
커스텀 셰이더 이미지 텍스처
먼저, Drei의 useTexture
hook을 사용하여 이미지 중 하나를 로드하고 현재 <meshBasicMaterial>
에 표시해 보겠습니다:
import { useTexture } from "@react-three/drei"; // ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const image = "textures/optimized/Default_authentic_futuristic_cottage_with_garden_outside_0.jpg"; const texture = useTexture(image); // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <meshBasicMaterial color="white" map={texture} /> </mesh> ); };
이미지가 평면에 잘 표시됩니다.
이제 이미지 간 전환과 호버 시 창의적인 효과를 추가하고자 하므로, 두 개의 이미지를 동시에 표시하고 애니메이션화할 수 있도록 커스텀 셰이더 material을 만듭니다.
ImageSliderMaterial
ImageSliderMaterial
이라는 커스텀 셰이더 material을 만들어 봅시다. 저는 이것을 ImageSlider
컴포넌트와 밀접하게 관련이 있으므로 같은 파일에 유지하기로 결정했습니다. 하지만 선호에 따라 별도의 파일로 만들 수도 있습니다.
// ... import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const ImageSliderMaterial = shaderMaterial( { uTexture: undefined, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /*glsl*/ ` varying vec2 vUv; uniform sampler2D uTexture; void main() { vec2 uv = vUv; vec4 curTexture = texture2D(uTexture, vUv); gl_FragColor = curTexture; }` ); extend({ ImageSliderMaterial, }); // ...
우리의 텍스처를 uTexture
라는 이름의 uniform
에 저장하고, 이를 프래그먼트 셰이더에 전달하여 표시합니다.
uTexture
uniform의 타입은 sampler2D
로, 2D 텍스처를 저장하는 데 사용됩니다.
특정 위치에서 텍스처의 색상을 추출하기 위해 texture2D
함수를 사용하고 uTexture
와 vUv
좌표를 전달합니다.
meshBasicMaterial
을 새 ImageSliderMaterial
로 교체해 봅시다:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} /> </mesh> ); };
이미지가 우리의 커스텀 셰이더 material을 사용하여 표시됩니다.
색상 그레이딩
눈이 매서워지기 시작한 것을 알고 있어요 🦅 이미지 색상 그레이딩이 다르게 보이는 것을 눈치챘군요!
이유는 <meshBasicMaterial/>
이 fragment shader 내부에서 선택된 tone mapping과 color space에 기반하여 색상을 조정하는 추가적인 처리를 수행하기 때문입니다.
이것은 우리가 커스텀 shader에서 수동으로 복제할 수 있는 것이지만, 이 레슨의 목표는 아니며 고급 주제입니다.
대신, 우리는 표준 Three.js material과 동일한 효과를 활성화하기 위해 사용할 수 있는 미리 준비된 fragments를 사용할 수 있습니다. meshBasicMaterial의 소스 코드를 보면 #include
문과 커스텀 코드의 혼합임을 알 수 있습니다.
코드를 쉽게 재사용하고 유지 보수할 수 있도록 Three.js는 다른 파일에서 코드를 포함하도록 전처리기를 사용합니다. 다행스럽게도 이러한 shader chunk를 우리의 커스텀 shader material에서도 사용할 수 있습니다!
두 줄을 fragment shader의 끝에 추가해 봅시다:
void main() { // ... #include <tonemapping_fragment> #include <encodings_fragment> }
shader chunks가 어떻게 작동하는지 더 잘 이해하기 위해 이 도구를 사용하여 include 문을 클릭하여 포함된 코드를 볼 수 있습니다: ycw.github.io/three-shaderlib-skim
meshBasicMaterial과 색상 그레이딩이 동일해졌습니다. 🎨
shader에 대해 더 나아가기 전에, UI를 준비해봅시다.
Zustand 상태 관리
Zustand는 애플리케이션 상태를 관리하기 위한 글로벌 스토어를 생성할 수 있게 해주는 작고 빠르며 확장 가능한 상태 관리 라이브러리입니다.
Redux나 컴포넌트 간 상태를 공유하고 복잡한 상태 로직을 관리하기 위한 사용자 정의 컨텍스트 솔루션의 대안으로 사용할 수 있습니다. (우리 프로젝트에서는 로직이 간단하더라도 말이죠.)
프로젝트에 Zustand를 추가해봅시다:
yarn add zustand
그리고 hooks
폴더에 useSlider.js
라는 새 파일을 만듭니다:
import { create } from "zustand"; export const useSlider = create((set) => ({}));
create
함수는 함수를 인수로 받아 set
함수를 통해 상태를 업데이트하고 병합합니다. 우리는 반환된 객체 안에 우리 상태와 메소드를 넣을 수 있습니다.
먼저 필요한 데이터를 정의해보겠습니다:
// ... export const useSlider = create((set) => ({ curSlide: 0, direction: "start", items: [ { image: "textures/optimized/Default_authentic_futuristic_cottage_with_garden_outside_0.jpg", short: "PH", title: "Relax", description: "Enjoy your peace of mind.", color: "#201d24", }, { image: "textures/optimized/Default_balinese_futuristic_villa_with_garden_outside_jungle_0.jpg", short: "TK", title: "Breath", color: "#263a27", description: "Feel the nature surrounding you.", }, { image: "textures/optimized/Default_desert_arabic_futuristic_villa_with_garden_oasis_outsi_0.jpg", short: "OZ", title: "Travel", color: "#8b6d40", description: "Brave the unknown.", }, { image: "textures/optimized/Default_scandinavian_ice_futuristic_villa_with_garden_outside_0.jpg", short: "SK", title: "Calm", color: "#72a3ca", description: "Free your mind.", }, { image: "textures/optimized/Default_traditional_japanese_futuristic_villa_with_garden_outs_0.jpg", short: "AU", title: "Feel", color: "#c67e90", description: "Emotions and experiences.", }, ], }));
curSlide
는 현재 슬라이드 인덱스를 저장합니다.direction
은 전환 방향을 저장합니다.items
는 슬라이드의 데이터를 저장합니다. (이미지 경로, 쇼트네임, 제목, 배경색, 설명)
이제 이전 및 다음 슬라이드로 이동하는 메서드를 만들어 봅시다:
// ... export const useSlider = create((set) => ({ // ... nextSlide: () => set((state) => ({ curSlide: (state.curSlide + 1) % state.items.length, direction: "next", })), prevSlide: () => set((state) => ({ curSlide: (state.curSlide - 1 + state.items.length) % state.items.length, direction: "prev", })), }));
set
함수는 새로운 상태를 이전 상태와 병합합니다. 모듈로 연산자를 사용하여 마지막 슬라이드에 도달했을 때 첫 번째 슬라이드로 돌아가도록 하거나 그 반대로 처리합니다.
이제 우리의 상태가 준비되었으니, UI를 준비해 보겠습니다.
슬라이더 UI
우리는 슬라이드 텍스트 세부 정보와 내비게이션 버튼을 표시하기 위해 Slider
라는 새로운 컴포넌트를 만들 것입니다. Slider.jsx
에서:
import { useSlider } from "./hooks/useSlider"; export const Slider = () => { const { curSlide, items, nextSlide, prevSlide } = useSlider(); return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* MIDDLE CONTAINER */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* TOP LEFT */} <div className="w-48 md:w-72 left-4 md:left-0 md:-translate-x-1/2 absolute -top-8 "> <h1 className="relative antialiased overflow-hidden font-display text-[5rem] h-[4rem] leading-[4rem] md:text-[11rem] md:h-[7rem] md:leading-[7rem] font-bold text-white block" > {items[curSlide].short} </h1> </div> {/* MIDDLE ARROWS */} <button className="absolute left-4 md:-left-14 top-1/2 -translate-y-1/2 pointer-events-auto" onClick={prevSlide} > <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8 stroke-white hover:opacity-50 transition-opacity duration-300 ease-in-out" > <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /> </svg> </button> <button className="absolute right-4 md:-right-14 top-1/2 -translate-y-1/2 pointer-events-auto" onClick={nextSlide} > <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8 stroke-white hover:opacity-50 transition-opacity duration-300 ease-in-out" > <path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" /> </svg> </button> {/* BOTTOM RIGHT */} <div className="absolute right-4 md:right-auto md:left-full md:-ml-20 bottom-8"> <h2 className="antialiased font-display font-bold text-transparent text-outline-0.5 block overflow-hidden relative w-[50vw] text-5xl h-16 md:text-8xl md:h-28" > {items[curSlide].title} </h2> </div> <div className="absolute right-4 md:right-auto md:left-full md:top-full md:-mt-10 bottom-8 md:bottom-auto"> <p className="text-white w-64 text-sm font-thin italic ml-4 relative"> {items[curSlide].description} </p> </div> </div> </div> ); };
사용된 CSS의 세부 사항에 대해 설명하지는 않겠지만 주요 요점을 설명하겠습니다:
- 중간 컨테이너는 3D 평면의 크기와 종횡비를 재현하는
div
입니다. 이를 통해 텍스트와 버튼을 평면에 상대적으로 배치할 수 있습니다. aspect-square
를 사용하여 컨테이너의 종횡비를 유지합니다.- 화살표 버튼은 Heroicons에서 가져왔습니다.
- 제목과 단축 이름은 고정된 크기를 가지고 있으며, 나중에 흥미로운 텍스트 효과를 생성하기 위해 오버플로우가 숨겨져 있습니다.
md:
클래스는 더 큰 화면에서 레이아웃을 조정하는 데 사용됩니다.
App.jsx
에서 Slider
컴포넌트를 Canvas
옆에 추가해 봅시다:
// ... import { Slider } from "./Slider"; function App() { return ( <> <main className="bg-black"> <section className="w-full h-screen relative"> {/* ... */} <Slider /> <Canvas camera={{ position: [0, 0, 5], fov: 30 }}> <color attach="background" args={["#201d24"]} /> <ImageSlider /> </Canvas> </section> {/* ... */} </main> </> ); } export default App;
슬라이더가 캔버스 앞에 표시됩니다.
Canvas
의 스타일을 배경으로 설정하고 전체 화면 너비와 높이를 차지하도록 변경해야 합니다:
{/* ... */} <Canvas camera={{ position: [0, 0, 5], fov: 30 }} className="top-0 left-0" style={{ // R3F에 의해 적용된 기본 스타일 재정의 width: "100%", height: "100%", position: "absolute", }} > {/* ... */}
index.css
에 사용자 정의 폰트와 스타일을 추가합시다:
@import url("https://fonts.googleapis.com/css2?family=Red+Rose:wght@700&display=swap&family=Poppins:ital,wght@1,100&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; @layer base { .text-outline-px { -webkit-text-stroke: 1px white; } .text-outline-0\.5 { -webkit-text-stroke: 2px white; } .text-outline-1 { -webkit-text-stroke: 4px white; } }
text-outline
클래스는 텍스트 주위에 윤곽선을 생성하는 데 사용됩니다.
사용자 정의 폰트를 추가하려면 tailwind.config.js
를 업데이트해야 합니다:
/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, fontFamily: { sans: ["Poppins", "sans-serif"], display: ["Red Rose", "sans-serif"], }, }, plugins: [], };
이제 예쁜 UI를 갖게 되었습니다:
텍스트 효과
전환을 더 흥미롭게 만들기 위해 제목, 짧은 이름 및 설명에 텍스트 효과를 추가할 것입니다.
먼저, 우리가 다음 슬라이드 또는 이전 슬라이드로 이동하는지 여부를 알기 위해 direction을 얻어야 합니다. useSlider
hook에서 이를 얻을 수 있습니다:
// ... export const Slider = () => { const { curSlide, items, nextSlide, prevSlide, direction } = useSlider(); // ... };
이전 슬라이드에서 표시된 텍스트를 애니메이션 하여 나오게 하고 새 텍스트가 들어오도록 하려면, 이전 슬라이드의 인덱스가 필요합니다. 이는 쉽게 계산할 수 있습니다:
// ... export const Slider = () => { // ... let prevIdx = direction === "next" ? curSlide - 1 : curSlide + 1; if (prevIdx === items.length) { prevIdx = 0; } else if (prevIdx === -1) { prevIdx = items.length - 1; } // ... };
이제 Framer Motion의 도움을 받아 텍스트 효과를 추가할 수 있습니다. 먼저 오른쪽 하단에 있는 제목부터 시작합니다:
// ... import { motion } from "framer-motion"; const TEXT_TRANSITION_HEIGHT = 150; export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* 가운데 컨테이너 */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* ... */} {/* 오른쪽 아래 */} <div className="absolute right-4 md:right-auto md:left-full md:-ml-20 bottom-8"> <h2 className="antialiased font-display font-bold text-transparent text-outline-0.5 block overflow-hidden relative w-[50vw] text-5xl h-16 md:text-8xl md:h-28" > {items.map((item, idx) => ( <motion.div key={idx} className="absolute top-0 left-0 w-full text-right md:text-left" animate={ idx === curSlide ? "current" : idx === prevIdx ? "prev" : "next" } variants={{ current: { transition: { delay: 0.4, staggerChildren: 0.06, }, }, }} > {item.title.split("").map((char, idx) => ( <motion.span key={idx} className="inline-block" // transform을 작동하게 하기 위해 (translateY) variants={{ current: { translateY: 0, transition: { duration: 0.8, from: direction === "prev" ? -TEXT_TRANSITION_HEIGHT : TEXT_TRANSITION_HEIGHT, type: "spring", bounce: 0.2, }, }, prev: { translateY: direction === "prev" ? TEXT_TRANSITION_HEIGHT : -TEXT_TRANSITION_HEIGHT, transition: { duration: 0.8, from: direction === "start" ? -TEXT_TRANSITION_HEIGHT : 0, }, }, next: { translateY: TEXT_TRANSITION_HEIGHT, transition: { from: TEXT_TRANSITION_HEIGHT, }, }, }} > {char} </motion.span> ))} </motion.div> ))} </h2> </div> {/* ... */} </div> </div> ); };
animate
prop을 사용하여 서로 다른 상태 간에 전환하고, variants
prop으로 각 상태의 속성을 정의했습니다.
각 문자를 애니메이션 하려면 제목을 문자 배열로 분할하고 staggerChildren
prop을 사용하여 각 문자 애니메이션의 지연을 줄 수 있습니다.
from
prop은 애니메이션의 시작 위치를 정의하는 데 사용됩니다.
제목에서 overflow-hidden
을 제거하여 효과를 확인하세요:
제목 텍스트가 들어오고 나가는 애니메이션이 실행됩니다.
같은 효과를 짧은 이름에 추가해보겠습니다:
// ... export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* 가운데 컨테이너 */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* 왼쪽 위 */} <div className="w-48 md:w-72 left-4 md:left-0 md:-translate-x-1/2 absolute -top-8 "> <h1 className="relative antialiased overflow-hidden font-display text-[5rem] h-[4rem] leading-[4rem] md:text-[11rem] md:h-[7rem] md:leading-[7rem] font-bold text-white block" > {items.map((_item, idx) => ( <motion.span key={idx} className="absolute top-0 left-0 md:text-center w-full" animate={ idx === curSlide ? "current" : idx === prevIdx ? "prev" : "next" } variants={{ current: { translateY: 0, transition: { duration: 0.8, from: direction === "prev" ? -TEXT_TRANSITION_HEIGHT : TEXT_TRANSITION_HEIGHT, type: "spring", bounce: 0.2, delay: 0.4, }, }, prev: { translateY: direction === "prev" ? TEXT_TRANSITION_HEIGHT : -TEXT_TRANSITION_HEIGHT, transition: { type: "spring", bounce: 0.2, delay: 0.2, from: direction === "start" ? -TEXT_TRANSITION_HEIGHT : 0, }, }, next: { translateY: TEXT_TRANSITION_HEIGHT, transition: { from: TEXT_TRANSITION_HEIGHT, }, }, }} > {items[idx].short} </motion.span> ))} </h1> </div> {/* ... */} </div> </div> ); };
그리고 설명에 간단한 페이드 인 및 아웃 효과를 추가합니다:
// ... export const Slider = () => { // ... return ( <div className="grid place-content-center h-full select-none overflow-hidden pointer-events-none relative z-10"> {/* 가운데 컨테이너 */} <div className="h-auto w-screen aspect-square max-h-[75vh] md:w-auto md:h-[75vh] md:aspect-[3/4] relative max-w-[100vw]"> {/* ... */} {/* 오른쪽 아래 */} {/* ... */} <div className="absolute right-4 md:right-auto md:left-full md:top-full md:-mt-10 bottom-8 md:bottom-auto"> <p className="text-white w-64 text-sm font-thin italic ml-4 relative"> {items.map((item, idx) => ( <motion.span key={idx} className="absolute top-0 left-0 w-full text-right md:text-left" animate={ idx === curSlide ? "current" : idx === prevIdx ? "prev" : "next" } initial={{ opacity: 0, }} variants={{ current: { opacity: 1, transition: { duration: 1.2, delay: 0.6, from: 0, }, }, }} > {item.description} </motion.span> ))} </p> </div> </div> </div> ); };
우리의 UI가 이제 애니메이션으로 준비되었습니다.
이제 이 강의의 가장 흥미로운 부분인 셰이더 전환 효과로 넘어갈 준비가 되었습니다! 🎉
이미지 전환 효과
텍스트를 애니메이션화할 때와 마찬가지로, 이미지 간의 전환을 수행하려면 현재 및 이전 이미지 텍스처가 필요합니다.
ImageSlider
컴포넌트에 이전 이미지 경로를 저장합시다:
// ... import { useSlider } from "./hooks/useSlider"; import { useEffect, useState } from "react"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { const { items, curSlide } = useSlider(); const image = items[curSlide].image; const texture = useTexture(image); const [lastImage, setLastImage] = useState(image); const prevTexture = useTexture(lastImage); useEffect(() => { const newImage = image; return () => { setLastImage(newImage); }; }, [image]); // ... };
useEffect
훅을 사용하여, 현재 이미지 경로를 lastImage
상태에 저장하고 이미지가 변경되면, 새로운 이미지 경로로 lastImage
상태를 업데이트합니다.
prevTexture
를 셰이더에서 사용하기 전에, 슬라이드를 변경할 때 깜빡임을 방지하기 위해 모든 이미지를 미리 로드해봅시다:
// ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... }; useSlider.getState().items.forEach((item) => { useTexture.preload(item.image); });
이렇게 함으로써, 모든 이미지를 미리 로드하게 되며, 웹사이트 초기 로딩 화면에 안전하게 로딩 화면을 추가하여 깜빡임을 방지할 수 있습니다.
이제 ImageSliderMaterial
에 이전 텍스처와 전환의 진행을 저장할 두 개의 uniforms를 추가해봅시다:
// ... const ImageSliderMaterial = shaderMaterial( { uProgression: 1.0, uTexture: undefined, uPrevTexture: undefined, }, /*glsl*/ ` // ... `, /*glsl*/ ` varying vec2 vUv; uniform sampler2D uTexture; uniform sampler2D uPrevTexture; uniform float uProgression; void main() { vec2 uv = vUv; vec4 curTexture = texture2D(uTexture, vUv); vec4 prevTexture = texture2D(uPrevTexture, vUv); vec4 finalTexture = mix(prevTexture, curTexture, uProgression); gl_FragColor = finalTexture; #include <tonemapping_fragment> #include <encodings_fragment> }` ); // ... export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial uTexture={texture} uPrevTexture={prevTexture} uProgression={0.5} /> </mesh> ); };
mix
함수를 사용하여 이전 텍스처와 현재 텍스처 간의 보간을 uProgression
uniform에 기반하여 수행합니다.
이전 이미지와 현재 이미지 간의 혼합을 확인할 수 있습니다.
페이드 인 및 아웃 효과
uProgression
유니폼을 애니메이션하여 이미지 간의 부드러운 전환을 만들어 봅시다.
먼저, uProgression
유니폼을 업데이트할 수 있도록 material에 대한 참조를 가져와야 합니다:
// ... import { useRef } from "react"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... const material = useRef(); // ... return ( <mesh> <planeGeometry args={[width * ratio, height * ratio]} /> <imageSliderMaterial ref={material} uTexture={texture} uPrevTexture={prevTexture} /> </mesh> ); };
우리가 수동으로 업데이트할 것이기 때문에 uProgression
prop을 제거할 수 있습니다.
이제 이미지가 변경될 때 useEffect
내에서 uProgression
을 0
으로 설정하고 useFrame
루프에서 1
로 애니메이션할 수 있습니다:
// ... import { useFrame } from "@react-three/fiber"; import { MathUtils } from "three/src/math/MathUtils.js"; export const ImageSlider = ({ width = 3, height = 4, fillPercent = 0.75 }) => { // ... useEffect(() => { const newImage = image; material.current.uProgression = 0; return () => { setLastImage(newImage); }; }, [image]); useFrame(() => { material.current.uProgression = MathUtils.lerp( material.current.uProgression, 1, 0.05 ); }); // ... };
이제 이미지 간의 부드러운 전환이 가능합니다.
이를 기반으로 더욱 흥미로운 효과를 만들어 보겠습니다.
왜곡된 위치
전환을 더욱 흥미롭게 만들기 위해 이미지를 전환의 방향으로 밀어낼 것입니다.
vUv
좌표를 사용하여 이미지의 위치를 왜곡할 것입니다. ImageSliderMaterial
에 uDistortion
유니폼을 추가하고 이를 사용하여 vUv
좌표를 왜곡해봅시다:
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.