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

  ..
  
  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);
}

Mandelbrot Fractal

x축은 실수부, y축을 허수로 생각하는 공간(복소수평면)에서의 원점에서 일정한 offset 값만큼 이동하여 제곱한 값에 대한 실수부와 허수를 각각 x, y축으로 삼아 픽셀값으로 시각화한 결과가 Mandelbrot Fractal이며 구현 코드와 그에 대한 결과는 아래와 같다.

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

void main() {
  vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;
  uv += vec2(0.08, 0.15);
  vec2 c = uv * 2.5 + vec2(-.69955, -.37999); // Offset
  vec2 z = vec2(0.);
  float iter = 0.;
  float max_iter = 60.;

  float h = 2. + sin(uTime);
  for(float i=0.; i<max_iter; i++) {
    z = vec2(
      z.x * z.x - z.y * z.y, // 실수부
      2. * z.x * z.y // 허수부
    ) + c;
    if(length(z) > 2.) break;

    iter++;
  }

  float f = iter / max_iter;
  f = pow(f, .75);
  vec3 col = vec3(f);

  gl_FragColor = vec4(col, 1.0);
}

2개의 모양 함수를 smooth하게 섞는 방법(smooth min/max)

위에서 a와 b는 모양 함수(Shape Function)인데, a와 b의 값에 대한 min 또는 max 값을 취하면 a와 b를 하나로 섞을 수 있다. 그런데 a와 b의 교차점에서 매우 날카롭게 섞이게 되는데, 이를 부드럽게 섞은 것이 세번째 함수의 결과이다. 세번째 함수를 보면 k, h, m으로 구성되는데 이 중 k의 값에 따라 얼마나 부드럽게 섞을 것인지 결정한다. k 값이 음수일때 max 값으로 섞고 양수일때 min 값으로 섞는다.

float smin(float a, float b, float k) {
  float h = clamp(.5 + .5 * (b - a) / k, 0., 1.);
  return mix(b, a, h) - k * h * (1. - h);
}

Ray-Sphere 계산

수식

코드

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

// v = a -> 0
// v = b -> 1 
// v = (a+b)/2 -> 0.5
float remap01(float a, float b, float v) {
  return (v - a) / (b - a);
}

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

  vec3 col = vec3(0);
  vec3 Ro = vec3(0);
  vec3 Rd = normalize(vec3(uv.x, uv.y, 1.));
  vec3 S = vec3(0, 0, 3);
  float R = 1.;

  float tp = dot(S - Ro, Rd);
  vec3 Ptp = Ro + Rd * tp;
  float y = length(S - Ptp);
  
  if(y < R) {
    float x = sqrt(R*R - y*y);
    float t1 = tp - x;
    float t2 = tp + x;

    float c = remap01(S.z, S.z - R, t1);
    col = vec3(c);
  }
  
  gl_FragColor = vec4(col, 1.0);
}

결과

Fragment Shader에서 3D 카메라

시작은 다음과 같다.

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

void main() {
   ?
}

프레그먼트의 uv 좌표는 좌측하단이 원점인데, 원점을 화면 중심으로 잡기 위해 다음 코드가 필요하다.

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

  float t = uTime; // 그냥 흘러가는 시간값
}

프레그먼트에서 무언가를 의미있게 표현하기 위해서는 그 무언가가 픽셀에서 얼마나 떨어져있는지의 거리값을 매우 의미있게 사용한다. 이를 위해 Ray가 필요한데, Ray는 시작점(ro)과 방향(rd)이 중요하다.

void main() {
  ..

  float t = uTime; // 그냥 흘러가는 시간값

  // Left-Hand (Z축은 모니터에서 사람을 향하는 방향이 마이너스임)
  vec3 ro = vec3(3. * sin(t), 1., -3. * cos(t)); // Ray의 시작점(카메라의 위치), 일단 시간에 따라 Y축으로 회전시켜본다.
}

Ray의 시작점인 ro는 보이는데, 방향인 rd는 아직 보이지 않는다. rd를 정하기 위해 카메라 개념을 이용한다.

void main() {
  ...

  vec3 ro = vec3(3. * sin(t), 1., -3. * cos(t)); // Ray의 시작점(카메라의 위치), 일단 시간에 따라 Y축으로 회전시켜본다.

  vec3 lookAt = vec3(.0); // 카메라가 바라보는 지점
  vec3 f = normalize(lookAt - ro); // 카메라가 바라보는 방향 벡터
  vec3 u = vec3(0,1,0); // 카메라의 Up 벡터 
  vec3 r = normalize(cross(u, f)); // 카메라의 Right 벡터 

  float zoom = 1.; // 확대 배율
  vec3 c = ro + f * zoom;
  vec3 i = c + uv.x * r + uv.y * u; // 교차하는 지점

  vec3 rd = normalize(i - ro); // Ray의 방향 벡터

이제 정육면체의 구성 정점 8개를 프레그먼트에 시각화하기만 하면 된다.

void main() {
  ...

  vec3 rd = normalize(i - ro); // Ray의 방향 벡터

  float d = 0.;  
  float off = .5;
  d += drawPoint(ro, rd, vec3(0.-off, 0.-off, 0.-off));
  d += drawPoint(ro, rd, vec3(0.-off, 0.-off, 1.-off));
  d += drawPoint(ro, rd, vec3(0.-off, 1.-off, 0.-off));
  d += drawPoint(ro, rd, vec3(0.-off, 1.-off, 1.-off));
  d += drawPoint(ro, rd, vec3(1.-off, 0.-off, 0.-off));
  d += drawPoint(ro, rd, vec3(1.-off, 0.-off, 1.-off));
  d += drawPoint(ro, rd, vec3(1.-off, 1.-off, 0.-off));
  d += drawPoint(ro, rd, vec3(1.-off, 1.-off, 1.-off));

  gl_FragColor = vec4(d);
}

drawPoint는 포인트를 그리는 함수이고 다음과 같다.

float drawPoint(vec3 ro, vec3 rd, vec3 p) {
  float d = distLine(ro, rd, p);
  d = smoothstep(.04, .03, d);
  return d;
}

distLine은 ro와 rd로 정의되는 Ray에서 프로그먼트 좌표 간의 거리를 얻는 함수인데 다음과 같다.

float distLine(vec3 ro, vec3 rd, vec3 p) {
  return length(cross(p - ro, rd)) / length(rd);
}

끝.

위의 코드 작성을 위해 학습한 내용