개인 프로젝트중 하나인 WorldStory-AI 의 배경화면 전환효과입니다.
react-three-fiber , glsl , gsap 가 사용되었습니다.
일반적으로 위와 같은 쉐이더 효과를 주기위해서는 Canvas
안에 존재하는 geometry에 쉐이더효과가 적용된 material을 사용해야합니다.
<div className="chat_background" style={{ backgroundImage: `url(${testbackground})` }}>
일반적으로 쓰는 div
태그나 img
태그가 사용되지 않습니다.
먼저 캔버스를 정의해보겠습니다.
<Canvas style={{ width: '100vw', height: '100vh', position: 'absolute' }}>
</Canvas>
캔버스는 전체화면을 기준으로 배치해주고 geometry와 material을 배치하기전 필요한 환경 설정을 해줍니다.
const Scene = ({ image1, image2 }) => {
return (
<>
<PerspectiveCamera makeDefault position={[0, 0, 1]} />
<ambientLight intensity={3} />
</>
);
};
Scene
이라는 컴포넌트안에 카메라와 빛을 적용합니다.
그다음 geometry의 재질을 결정하는 mateiral을 정의해야하는데, 일반적으로 THREE.JS에서 사용하는
MeshBasicMaterial
MeshStandardMaterial
MeshPhongMaterial
같이 기본적으로 제공하는 material을 사용하는게 아니라 직접 material을 정의 해야합니다.
다행히 아예 구조 자체를 처음부터 설계하는게 아닌 shader material을 정의하는데 필요한 vertex와 fragment 만 정의합니다.
그 외의 나머지 것들은 react-three-drei 에서 불러오면 알아서 정의해줍니다.
import { PerspectiveCamera, shaderMaterial, useAspect, useTexture } from '@react-three/drei';
shaderMaterial을 정의하고 react에서 사용하기 위해서는 glslify의 glsl
모듈과 react-three-fiber의 extend
모듈이 추가적으로 사용됩니다.
import { PerspectiveCamera, shaderMaterial, useAspect, useTexture } from '@react-three/drei';
import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
import glsl from 'glslify';
const ImageTransitionMaterial = shaderMaterial(
// uniforms
{
},
// vertex
glsl`
`,
// fragment
glsl`
`
);
extend({
ImageTransitionMaterial,
});
ImageTransitionMaterial
이라는 shaderMateiral의 뼈대가 만들어졌습니다.
이제 재질의 정점 위치를 반환하는 vertex 와 재질의 최종 색상값을 결정하는 fragment 마지막으로 리액트에서 전달하는 값을 매개변수로 받는 uniforms 를 정의합니다.
유니폼은 3개를 받을겁니다.
어떤 이미지를 사용하고 있는지 : currnetImage
어떤 이미지를 사용할 건지 : nextImage
이미지 전환 조절 변수 : dispFactor
// uniforms
{
dispFactor: 0,
currentImage: new THREE.Texture(),
nextImage: new THREE.Texture(),
},
위에서 말했다시피 이미지는 div
태그나 img
태그로 보여주는게 아니라 three.js의 geometry와 material로 받는거기때문에 이미지들의 타입은
THREE.Texture()
로 정의해줍니다.
물론 전환할 이미지들도 three.js texture로 변환해주어야합니다.
// useTexture는 react-three-drei 모듈을 import할떄 속해있습니다.
const prevImage = useTexture(image1);
const nextImage = useTexture(image2);
그다음 정점의 위치를 반환해야합니다.
glsl에서 정점의 위치를 전달하는데 가장 많이 사용되는 값은
varying vec2 vUv;
입니다.
varying : vertex에서 fragment로 값을 전달하겠다
vec2 : 2차원 벡터다(이미지)
vUv : 변수이름은 이렇게 하겠다
// vertex
glsl`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 0.7 );
}
`,
gl_position
은 최종적으로 계산된 정점의 위치를 나타내는 GLSL 내장변수입니다.
projectionMatrix
와 modelViewMatrix
는 three.js에서 제공하는 행렬입니다.
position
은 현재 정점의 3D 좌표를 나타내는 glsl 내장변수입니다.
gl_position은 4차원 벡터를 입력값으로 받으니 최종적으로 정점의 위치는
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 0.7 );
인 4차원 벡터를 반환합니다.
// fragment
glsl`
varying vec2 vUv;
uniform sampler2D currentImage;
uniform sampler2D nextImage;
uniform float dispFactor;
void main() {
vec2 uv = vUv;
vec4 _currentImage;
vec4 _nextImage;
float intensity = 0.6;
vec4 orig1 = texture2D(currentImage, uv);
vec4 orig2 = texture2D(nextImage, uv);
_currentImage = texture2D(currentImage, vec2(uv.x, uv.y + dispFactor * (orig2 * intensity)));
_nextImage = texture2D(nextImage, vec2(uv.x, uv.y + (1.0 - dispFactor) * (orig1 * intensity)));
vec4 finalTexture = mix(_currentImage, _nextImage, dispFactor);
gl_FragColor = finalTexture;
}
`
vertex에서 사용된 vUv
, uniform에서 받을 이미지2개, 전환 조절변수를 전달받았습니다.
vec4 orig1 = texture2D(currentImage, uv);
vec4 orig2 = texture2D(nextImage, uv);
2개의 이미지에서 uv
좌표에 해당하는 색상을 샘플링하여 4차원 벡터 변수에 저장합니다.
_currentImage = texture2D(currentImage, vec2(uv.x, uv.y + dispFactor * (orig2 * intensity)));
_nextImage = texture2D(nextImage, vec2(uv.x, uv.y + (1.0 - dispFactor) * (orig1 * intensity)));
uniforms에서 받은 dispFactor
와 intensity를 사용하여 두 이미지 간에 전환효과를 적용합니다.
vec4 finalTexture = mix(_currentImage, _nextImage, dispFactor);
3개의 변수를 혼합하여 최종 변수를 만듭니다.
gl_FragColor = finalTexture;
gl_FragColor
는 glsl 내장함수로 최종 색상을 화면에 표시하는 변수입니다.
const ImageTransitionMaterial = shaderMaterial(
// uniforms
{
dispFactor: 0,
currentImage: new THREE.Texture(),
nextImage: new THREE.Texture(),
},
// vertex
glsl`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 0.7 );
}
`,
// fragment
glsl`
varying vec2 vUv;
uniform sampler2D currentImage;
uniform sampler2D nextImage;
uniform float dispFactor;
void main() {
vec2 uv = vUv;
vec4 _currentImage;
vec4 _nextImage;
float intensity = 0.6;
vec4 orig1 = texture2D(currentImage, uv);
vec4 orig2 = texture2D(nextImage, uv);
_currentImage = texture2D(currentImage, vec2(uv.x, uv.y + dispFactor * (orig2 * intensity)));
_nextImage = texture2D(nextImage, vec2(uv.x, uv.y + (1.0 - dispFactor) * (orig1 * intensity)));
vec4 finalTexture = mix(_currentImage, _nextImage, dispFactor);
gl_FragColor = finalTexture;
}
`
);
extend({
ImageTransitionMaterial,
});
react-three-drei 의 shaderMaterial
로 기본뼈대를 만들고
glsl 코드로 정점의 위치와 최종 색상을 반환한뒤
react-three-fiber의 extend
모듈로 react에서 사용할 수 있게 만들었습니다.
material을 만들었으니 이제 geometry를 만들 차례입니다. 저는 전환효과를 전체 배경화면에 적용되도록 만들예정이라 plane geometry 의 크기를 사용자의 전체 화면에 맞게 크기를 맞출겁니다.
const { size } = useThree();
const scale = useAspect(size.width, size.height, 1);
const materialRef = useRef();
react-three-drei 에서 복잡한 계산할 것없이 useAspect
로 사용자 화면에 맞는 크기를 반환하는 함수를 제공합니다.
return (
<mesh position={[0, 0, 0]} scale={scale}>
<planeGeometry />
<imageTransitionMaterial ref={materialRef} />
</mesh>
);
이제 최종 mesh
를 정의합니다.
ImageTransitionMaterial
로 shader material을 정의한것을 react-three-fiber의 extend
를 사용할 경우, return문안에서는 첫 글자가 소문자여야지만 적용됩니다.
위의 return문에서는
x,y,z 좌표가 0이고 크기가 사용자 화면에 맞는 shader가 적용된 네모난 물체
가 되겠습니다.
이제 material에 정의된 ref 를 통해 shader material의 uniforms를 ref 값으로 전달해주면 이미지 전환 효과가 완성됩니다.
const [animationFlag, setAnimationFlag] = useState(false);
const ChangeImage = () => {
const currentValue = materialRef.current.uniforms.dispFactor.value;
setAnimationFlag(true);
gsap.to(materialRef.current.uniforms.dispFactor, {
value: currentValue === 0 ? 1 : 0,
duration: 1,
ease: 'power2.out',
oninit: () => {
if (currentValue === 1) {
materialRef.current.uniforms.currentImage.value = nextImage;
materialRef.current.uniforms.nextImage.value = prevImage;
} else {
materialRef.current.uniforms.currentImage.value = prevImage;
materialRef.current.uniforms.nextImage.value = nextImage;
}
},
onComplete: () => {
setAnimationFlag(false);
},
});
};
useEffect(() => {
if (animationFlag === false) {
ChangeImage();
}
}, [nextImage]);
여기에서 gsap 라이브러리가 사용됩니다. dispFactor를 1초에 걸쳐 0에서 1 또는 1에서 0까지 조절하면서 이미지 두개를 변경하는 기능입니다.
선생님 혹시 유튜브에 ComfyUI 스크립트들이랑 주석 공유 안될까요....? ㅠㅠ
wonbeom2669@gmail.com 구글 드라이브 링크 희망합니다 ㅠㅠㅠㅠ 공부하면서 참고해서 보고싶어요 ㅠㅠㅠ