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가 광원의 표현에 더욱 적합하다.

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

결과

code snippet (20250417,21)

  // if(p.x > .5 || p.y > .5) col.r = .8;
  if(max(p.x, p.y) > .5) col.r = .8;
  // col.r = step(.5, max(p.x, p.y));
  float a = radians(45.);
  vec2 n = vec2(sin(a), cos(a));
  float d = dot(uv, n);
  d = abs(d);
  col += smoothstep(fwidth(d), 0., d-.001);
function prepareOneFish() {
  let start = new Date().getTime();
  while (new Date().getTime() < start + 1000) {
    // preparing fish
  }
}
function setTimeoutPromise(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay);
  });
}

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}