안녕하세요.
메타볼은 제가 정말 좋아하는 그래픽 모델입니다.
연속적이고, 부드럽지만 동시에 끈끈한 느낌을 주는 것이 인상적인데요.
하지만 이 메타볼을 웹 상에서 단순하게 렌더링하는 것은 많은 한계가 있습니다.
23/10/20 추가) 후술할 한계가 있는 줄 알았는데, 좀 더 시도해보니 아니었습니다.
가장 간단한 방법은 svg filter
를 이용해서 구현하는 방법이 있지만,
아래와 같이 성능이 굉장히 좋지 않고,
메타볼 효과가 매우 잘 깨진다는 단점이 있습니다.
따라서 WebGL을 이용해서 구현한다면,
성능은 물론이고, 정확도도 동시에 챙길 수 있을거라 생각했습니다.
svg filter
를 이용하더라도 정확하고 효과적으로 렌더링하는 방법이 있습니다.
무엇보다 이 방법의 장점은 html 태그를 이용하여 렌더링하기 때문에,
애니메이션 구성에 있어서 손쉽게 GPU의 힘을 빌릴 수 있다는 최대 장점이 있습니다.
하지만, 구성하는 애니메이션에서 다뤄야할 오브젝트들이 많아진다면
성능적으로 WebGL을 사용하는 것이 대안이 될 수 있습니다.
const blur = 12;
const alpha = blur * 6;
const r = "1 0 0 0 0";
const g = "0 1 0 0 0";
const b = "0 0 1 0 0";
const a = `0 0 0 ${alpha} ${alpha / -2}`;
후술할 방법론을 읽어보면 알겠지만(먼저 후술한 방법론을 읽으시는 것을 추천드립니다),
r
, g
, b
, a
를 feColorMatrix
태그에 값으로 넣어줄건데 저렇게 설정한 이유는 다음과 같습니다.
r
, g
, b
는 각 픽셀의 RGB값을 그대로 사용하기 위해 저렇게 설정했습다.
문제는 a
인데, MDN의 feColorMatrix
문서에 따르면 필터처리 된 각 픽셀의 alpha 값
이 도출되는 식을 저 값에 대입해보면 아래와 같이 나옵니다.
필터처리 된 픽셀의 투명도 값 = 블러처리 된 픽셀의 투명도 값 x alpha - alpha x 0.5
어차피 모든 투명도 값은 0~1 사이이므로,
0 <= 블러처리 된 픽셀의 투명도 값 <= 1
을 만족하고, 곧
0 <= 블러처리 된 픽셀의 투명도 값 x alpha <= alpha
-0.5 x alpha <= 필터처리 된 픽셀의 투명도 값 <= 0.5 x alpha
라는 범위가 나옵니다.
하지만, 필터처리된 픽셀의 투명도 값의 범위도 마찬가지로
0 <= 필터처리 된 픽셀의 투명도 값 <= 1
이므로, 음수 범위로 계산된 값들은 전부 투명도가 0이 되기에 화면에 그려지지 않습니다.
결과론적으로 후술할 alpha-thresholding
을 0.5를 기준으로 수행하는 것이죠.
또한, 기본적으로 설정해준 alpha
의 값이 굉장히 크기 때문에(블러처리에 대응하기 위해) 메타볼이 깨지거나 이상하게 렌더링 될 걱정도 없게 되고요.
이 값들을 이용하여 아래 svg filter
를 아래와 같이 만들어줍니다.
<svg style="position: absolute">
<defs>
<filter id="filter" colorInterpolationFilters="sRGB">
<feGaussianBlur stdDeviation="${blur값}" />
<feColorMatrix values="${r값} ${g값} ${b값} ${a값}" />
<feComposite in='SourceGraphic' />
</filter>
</defs>
</svg>
원래는 feComponentTransfer
와 feFuncA
태그를 이용하여 렌더링하는 방법을 사용하였지만, feColorMatrix
태그 하나로 변경하였습니다.
feComposite
은 그라데이션 효과를 위해 사용했습니다. 없어도 됩니다!
.container {
filter: url(#filter);
}
filter
태그에서 적었던 id 값을 이용하여 만든 svg filter
를 원하는 컨테이너에 적용해줍니다.
저는 WebGL로 PIXI.js를 이용했고,
PIXI.js의 filter를 이용하여 메타볼을 구현했습니다.
메타볼 효과는 두 가지 filter들을 합쳐 간단하게 만들어낼 수 있습니다.
https://pixijs.com/examples/filters-basic/blur
화면에 가우시안 블러 효과를 제공합니다.
CSS의 filter: blur()
함수와 동일한 효과를 얻습니다!
투명도에 따라 원하는 임계처리(thresholding)를 제공합니다.
그런데 문제는, PIXI.js에 이런 필터는 없습니다.
따라서 제가 직접 GLSL을 이용하여 커스텀 filter를 제작해주었습니다.
import { Filter } from "pixi.js";
export function useAlphaThresholdFilter({
threshold, r, g, b
}: {
threshold: number;
r: number;
g: number;
b: number;
}) {
const filter = new Filter(
undefined,
`
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float threshold, r, g, b;
void main() {
vec4 color = texture2D(uSampler, vTextureCoord);
vec3 maskColor = vec3(r, g, b) / 255.0;
if (color.a > threshold) {
gl_FragColor = vec4(maskColor, 1.0);
} else {
gl_FragColor = vec4(vec3(0.0), 0.0);
}
}
`,
{ threshold, r, g, b }
);
return filter;
}
이 filter들을 아래와 같은 방법으로 적용해주었습니다.
3번 코드의 내용이 커스텀 filter 코드의 내용입니다!
이 filter들을 적용하면 아래와 같은 효과를 얻을 수 있습니다.
가장 핵심은 blur 처리를 했을 때,
기존 색상의 opacity가 외곽으로 갈수록 낮아진다는 것 인데요.
그렇기 때문에 여러 원들이 겹칠 때 겹치는 부분은 opacity가 더 높아지므로 alpha-thresholding을 적용하면 부드럽게 떨어지는 윤곽선을 만들어낼 수 있는 트릭인거죠.
svg filter
로 메타볼을 만들었을 때 가장 큰 문제였던 정확도 문제가 말끔하게 해결됩니다.
게다가 성능도 많이 잡아먹지 않습니다!
원래는 단순히 여기까지 만들었는데,
blur를 얼마나 해줄지,
alpha-thresholding의 임계치를 얼마로 해줄지에 따라
아래처럼 서로 다른 메타볼 효과가 구현되는 것을 확인할 수 있었는데요.
여기서 끝내기엔 아쉽죠.
그래서 메타볼 파라미터를 커스터마이징 할 수 있는 컨트롤러를 만들어
다양한 메타볼 효과를 구현할 수 있게 프로젝트를 구성했습니다.
직접 프로젝트를 들어가서 체험해보세요!
감사합니다!
오.. 신기하군요.