The Avener
The Avener - Beautiful | New song and music video
Dive into interactive images from The Avener’s new music video.
beautiful.theavener.com
Intro.
개인적으로, 사이버 펑크/ 네온 느낌이 강한 디자인을 좋아하는데, Avener 사이트의 이펙트가 앞서 말한 디자인과 잘 어올릴 것 같은 생각에 분석 주제로 삼았다.
코드 기반은 Three.js를 기반으로 하나, 모든 핵심 로직이 webGL 코드로 이루어지기에, 제목에서 ThreeJS는 제거하였다.
이펙트는 다음과 같은 요소들의 연결로 이루어진다.
- 미디어 1(cover) 와 미디어 2(target) 간의 전환 (T1)
- 외부 팩터(시간 etc)에 크기를 의존하는 원 (T2)
- 물결(ripple) (T3)
T1 : Transition
Web API과 유일하게 상호 작용하는 파트이다.
먼저 ShaderMaterial에 cover와 target의 텍스쳐를 넘긴다. (참고로 필자는 geometry로는 PlaneGeometry를 사용하였다)
// image & video는 DOM 객체
new THREE.ShaderMaterial({
uniforms: {
uImage: {
value: new THREE.TextureLoader().load(image),
},
uVideo: {
value: new THREE.VideoTexture(video);,
}
}
}
이제 vertex shader에서는 uv 좌표만 varying 키워드를 통해 fragment shader로 넘겨준다.
//vertex.glsl
varying vec2 vUv;
void main(){
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
fragment 쉐이더에서 이제 uv 값을 받아 uniform으로 넘긴 미디어 데이터를 렌더링한다.
//fragment.glsl
varying vec2 vUv;
uniform sampler2D uImage;
uniform sampler2D uVideo;
void main(){
gl_FragColor = texture2D(uVideo, vUv);
}
잘 나오는 것 같으나, 브라우저 크기를 줄이거나 늘리면 이미지가 찌그러진다.
이상적인 결과는 무엇일까?
일단 원본 미디어의 비율을 유지해야한다.
현재 필자가 cover로 삼고있는 이미지와 비디오는 모두 (1920 x 1080)인데 (즉, 기본적으로 브라우저보단 크기가 크다), 너무 해상도가 작은 경우도 고려해야한다.
생각해보면 이 문제의 경우, css로 스타일링 할 경우 `object-fit: cover` syntax를 통해 해결한다.
아래는, W3C에서 정의한 스펙이다.
즉, 정리하면 원본의 비율을 유지하면서 요소를 꽉채울 때 까지 확대 / 축소한다.
이를 webGL로 표현한다면 uv 좌표계를 확대 / 축소하라는 뜻이고, 이를 위해 위 스펙에 의거하여 다음 2가지 정보가 필요하다.
- 원본 미디어의 비율
- 컨텐츠 박스의 비율
일단 필자의 경우, 컨텐츠 박스로 브라우저 전체를 사용할 예정이기에, 브라우저의 비율을 fragment 쉐이더로 넘길 것이다.
마찬가지로 uniform을 통해 데이터를 넘긴다.
new THREE.ShaderMaterial({
...,
uniforms: {
...,
uViewport: {
value: new THREE.Vector2(document.body.offsetWidth, document.body.offsetHeight),
},
uMediaDimension: {
value: new THREE.Vector2(1980, 1080),
},
...
}
}
varying vec2 vUv;
uniform sampler2D uVideo;
uniform vec2 uViewport;
uniform sampler2D uImage;
uniform vec2 uMediaDimension;
void main(){
float medioRatio = uMediaDimension.x / uMediaDimension.y;
float viewportRatio = uViewport.x / uViewport.y;
}
이제 경우의 수가 2가지로 나뉘는데, 앞서 설명했듯이 원본 미디어의 크기가 브라우저보다 큰 경우 / 작은 경우로 나뉘므로 해당 부분에 대한 분기를 넣어서 resizeFactor를 구하자.
또한, 미디어의 중심 (center, center)를 기준으로 리사이징 하고 싶으므로, uv좌표계를 먼저 vec2(-0.5)로 평행이동 후 리사이징 후 다시 vec2(0.5)만큼 평행이동 시킨다.
이 과정을 그림으로 표현하자면 다음과 같다. (손으로 직접 그렸기에 퀄리티가 안좋습니다.. 이해 안가시면 댓글 남겨주세요)
참고로 평행이동 안하고 단순히 비율만 곱하면 다음 그림과 같다.
나는 지렁이의 중심을 보고 싶은데, 왼쪽꼬리만 보여지게 된다.
정리하여 코드로 나타내면 다음과 같다.
void main(){
vec4 video= texture2D(uVideo, vUv );
vec4 image = texture2D(uImage, vUv);
float mediaRatio = uMediaDimension.x / uMediaDimension.y;
float viewportRatio = uViewport.x / uViewport.y;
vec2 resizeFactor = vec2(1.);
if(mediaRatio > viewportRatio){
resizeFactor = vec2(viewportRatio / mediaRatio, 1.);
}else{
resizeFactor = vec2(mediaRatio / viewportRatio, 1.);
}
vec2 scaledUV = (vUv - vec2(0.5))* resizeFactor + vec2(0.5);
gl_FragColor = texture2D(uVideo , scaledUV);
}
짠! 이제 찌그러지지 않는다.
솔직히 T1의 과정에서 반응형 맞추는게 가장 어렵지, 가장 코어인 전환은 딱 한줄로 가능하다.
scaleFactor에 따라 cover와 target을 mix해주면 된다.
코드는 다음과 같다.
uniform float scaleFactor;
void main(){
...
vec4 video= texture2D(uVideo, scaledUV );
vec4 image = texture2D(uImage, scaledUV);
vec4 effect = mix(image, video, scaleFactor);
gl_FragColor = effect;
}
T2 : Circle
먼저 복잡하게 생각하지말고 단순하게 원을 그려보자.
원은 중심으로 부터 거리가 일정한 점들의 집합으로 테두리를 정의하고, 내부에 있는 점들을 영역으로 삼는다.
이런 성질을 이용해, uv 평면상에서 각 픽셀의 벡터값의 크기 (length)를 가지고 원을 표현할 수 있다.
필자의 경우, 원을 화면에 중심에 놓고 싶기에, 앞서 설명했듯 uv 평면 평행이동 후, 그 결과값을 가지고 벡터 크기를 연산한다.
vec2 circleUV = (vUv - vec2(0.5)) * vec2(1., 1./viewportRatio);
gl_FragColor = vec4(0.,0.,0.,length(circleUV));
결과물을 보면, 모든 픽셀들이 보간되어 경계가 뚜렷하진 않으나, 원의 형태를 이루고 있는 것을 볼 수 있다.
참고로 circleUV에 곱한 뷰포트 비율값 경우, T1과 마찬가지로 원이 찌그러지거나 늘려짐을 막기 위해서이다.
이제 그러면, 원의 경계를 명확히 하면 될듯하다.
단, 원래 애니메이션에서는 경계가 원의 크기가 커질 수록 모호해지는 경향이 있다.
이를 위해 webgl 빌트-인 함수로 smoothstep을 사용하자.
원의 크기에 따라서, 원의 경계가 점차 모호해지기에, 원의 경계는 곧, 원의 크기 팩터에 의존한다.
따라서, 다음의 코드로 나타내어진다.
uniform float uCircleScale;
...
void main(){
...
vec2 circleUV = (vUv - vec2(0.5)) * vec2(1., 1./viewportRatio);
float radius = uCircleScale;
float opaque = 0.5 * uCircleScale;
float circle = smoothstep(
radius - radius * opaque,
radius + radius * opaque,
10. * dot(circleUV, circleUV)
);
vec4 effect = mix(image, video, circle);
gl_FragColor = effect;
}
smoothstep의 파라미터로 벡터의 크기 값을 넣어주는게 직관적일 수 있으나, 연산 속도 면에서 dot 연산을 사용하는게 더 빠르다.
T3 : Ripple
가장 막막했던 구간인데, 개인적으로 알고 있는 노이즈가 펄린(Perlin)노이즈 밖에 없었기에 처음에 해당 노이즈로 시도했다가, 결과가 전혀 이쁘지 않았어서 난감했다.
리서치 중 FBM(Fractal Brownian Motion) 노이즈에 대해 알게 되었고 해당 알고리즘을 이용해 구현했다. 검색해보면 다양한 FBM이 있는데, 마음에 드는것 사용하면 된다. 필자는 해당 노이즈를 사용했다.
먼저 물결 효과를 cover에 넣어야하기에, cover 정의 시, 해당 uv에 물결 효과를 넣는다.
참고로 코드의 각종 인자값들은 실험적인 결과로 얻어낼 수 있다.
uniform float uTime;
void main(){
...
vec2 center = vUv - vec2(0.5);
float ripple = 20. *fbm(center * fbmLow(vec2(length(center) - uTime / 6. )));
vec2 rippleDistortion = fbmLow(center * swirl) * center * 14.;
vec2 circleUV = (vUv - vec2(0.5)) * vec2(1., 1./viewportRatio);
float radius = uCircleScale;
float opaque = 0.5 * uCircleScale;
float circle = smoothstep(
radius - radius * opaque,
radius + radius * opaque,
3. * dot(circleUV, circleUV)
);
vec2 backgroundUV= scaledUV + (0.5 * rippleDistortion - center * circle);
vec4 video= texture2D(uVideo, scaledUV );
vec4 image = texture2D(uImage, backgroundUV);
vec4 effect = mix( video, image,circle);
}
'Gen Art' 카테고리의 다른 글
Shattering Text Animation 제작기 (2) | 2024.06.06 |
---|---|
[Three.js] case study: m-trust (1) | 2023.07.31 |
[Javascript] 폭죽 이펙트 (fireworks) (3) | 2023.05.29 |