R3F Cookbook

R3F는 React 기반에서 three.js를 매우 직관적이고 사용하기 쉬운 컴포넌트 레벨로 개발할 수 있는 패키지입니다. 아래의 영상은 R3F에서 제공하는 컴포넌트, 클래스 등과 R3F를 위한 보다 더 발전된 컴포넌트를 제공하는 drei에 대한 내용을 설명합니다. 이 영상 제작의 목적은 궁극적으로 drei의 컴포넌트를 이해하고 이러한 컴포넌트를 직접 개발하기 위한 지식을 얻기 위함입니다.

위의 영상에 더해 지속적으로 내용이 업데이트되므로 해당 채널에 방문하여 참고하시기 바랍니다.

CSS에서의 광원 효과

DOM 구성은 다음과 같다.

CSS는 다음과 같다.

body {
  background: #bdcddb;
}

.loader {
  position: relative;
  width: 300px;
  height: 300px;
}

.circle {
  position: absolute;
  inset: 35px;
  background-color: #acbaca;
  border-radius: 50%;
  box-shadow: 5px 5px 15px 0 #152b4a66,
    inset 5px 5px 5px rgba(255,255,255,0.55),
    -6px -6px 10px rgba(255,255,255,1);
}

.circle::before {
  content: '';
  position: absolute;
  inset: 4px;
  background: linear-gradient(#2196f3, #e91e63);
  border-radius: 50%;
  animation: animate 2s linear infinite;
}

@keyframes animate {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(350deg);
  }
}

.circle::after {
  content: '';
  position: absolute;
  inset: 35px;
  background: #acbaca;
  border-radius: 50%;
}

.circle span {
  position: absolute;
  inset: 4px;
  background: linear-gradient(#2196f3, #e91e63);
  border-radius: 50%;
  animation: animate 2s linear infinite;
  filter: blur(20px);
  z-index: 1000;
  /* mix-blend-mode: color-dodge; */
  mix-blend-mode: plus-lighter;
}

.circle span::before {
  content: '';
  position: absolute;
  inset: 40px;
  background: #bdcbdb;
  border-radius: 50%;
  animation: animate 2s linear infinite;
  z-index: 1000;
}

결과는 다음과 같다.

CSS의 67번째 줄이 핵심인데, 만약 해당 코드를 제거하면 결과는 다음과 같다.

mix-blend-mode의 값인 plus-lighter는 아직 공식적으로 문서화가 되지 않았고 만약 작동하지 않는다면 color-dodge를 사용하기 바란다. 참고로 color-dodge보다 plus-lighter가 광원의 표현에 더욱 적합하다.

이웃 격자 밖으로 출력된 결과에 대한 자연스러운 처리

  ..
  
  float n = Hash21(id);
  col += Star(gv - vec2(n, fract(n * 34.)) + .5, 1.);
  
  ..

위의 결과를 보면 격자 하나에 대해 어떤 형상 하나가 표시되어 있다. 문제는 형상 하나가 외부 격자 밖에서는 짤려나간다는 것인데, 이를 해결하기 위한 코드가 아래와 같다.

  ..
  
  for(int y=-1; y<=1; y++) {
      for(int x=-1; x<=1; x++) {
        vec2 offs = vec2(x, y);
        
        float n = Hash21(id + offs);
        col += Star(gv - offs - vec2(n, fract(n * 34.)) + .5, 1.);
      }
  }
  
  ..

위의 코드에서 유의해야할 점은 형상 하나가 바로 인접한 이웃의 이웃 밖으로 나갈 경우 처리되지 않는다. 이럴때는 밖으로 나간 것까지 포함되도록 for 문의 반복 범위를 확장해야 한다.

전체 코드는 다음과 같다.

uniform vec3 uResolution;
uniform float uTime;
uniform vec4 uMouse;

mat2 Rot(float a) {
  float s = sin(a);
  float c = cos(a);
  return mat2(c, -s, s, c);
}

float Star(vec2 uv, float flare) {
  float d = length(uv);
  float m = .05 / d;
  
  float rays = max(0., 1. - abs(uv.x * uv.y * 1000.));
  m += rays * flare;
  uv *= Rot(3.1415 / 4.); 
  rays = max(0., 1. - abs(uv.x * uv.y * 1000.));
  m += rays * .3 * flare;

  m *= smoothstep(1., .0, d); // 형상 하나가 바로 인접한 이웃 밖으로 나가지 않도록 해주는 코드

  return m; 
}

float Hash21(vec2 p) {
  p = fract(p * vec2(123.34, 456.21));
  p += dot(p, p + 45.32);
  return fract(p.x * p.y);
}

void main() {
  vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;
  uv *= 3.;

  vec3 col = vec3(0);

  vec2 gv = fract(uv) - .5;
  vec2 id = floor(uv);

  for(int y=-1; y<=1; y++) {
      for(int x=-1; x<=1; x++) {
        vec2 offs = vec2(x, y);
        
        float n = Hash21(id + offs);
        col += Star(gv - offs - vec2(n, fract(n * 34.)) + .5, 1.);
      }
  }

  if(gv.x > .48 || gv.y > .48) col.r = 1.;  

  gl_FragColor = vec4(col, 1.0);
}