Study Lab

유리 나뭇잎이 떨어지는 풍경

하루종일 만들어서 도전해보았다.

Makonea
··25분
Step 1 Lab experiment
Loading lab experiment...

Glass Leaf Fall

Warning

해당 글은 AI로 대부분의 코드가 작성됨을 알림.

글은 사람이 작성했으나, 수식 및 및 코드 대부분 AI가 작성함.

단풍잎의 형태 발생, N-BK7 유리 광학, 얇은 판의 준정상 낙하, 비압축 난류, 유리 미립자의 유한 Reynolds 수 항력을 한 화면 안에 결합한 WebGL2 연구 데모

AI의 성능이 놀랍다. 실제로 논문을 그대로 박아두니까 거의 그대로 나왔다.

이 작품은 “실제 유리 단풍잎을 그대로 계산했다”고 주장하지 않는다. 공기역학에는 실제 크기와 질량을 사용하지만, 눈에 보이는 굴절을 만들기 위한 광학 두께는 의도적으로 과장하는데, 잎맥은 식물 형태 모델에서 생성하고, 광학 표면은 그 맥을 유리 릿지로 해석한다.


1. 모델 범위와 사실의 소유자

이 데모가 계산하는 것

사실의 소유자

잎 윤곽

5개 갈래, 호길이로 배치한 톱니, 잎자루

tools/leaf-model.mjs, shaders/leaf-outline.glsl

잎맥

공간 식민화와 Murray 반지름 법칙으로 생성한 3개 망

tools/leaf-model.mjsshaders/leaf-venation.glsl

유리 재료

SCHOTT N-BK7 Sellmeier 굴절률과 RGB 분산

js/glass.js

광학 표면

타원체 캡, 잎맥 릿지, 기울기와 Hessian

shaders/leaf-thickness.glsl

낙하

2D 타원판의 부가질량, Munk 모멘트, 준정상 양력·항력

js/dynamics.js

공기

유선함수로 만든 6옥타브 발산 없는 속도장

js/air.js

유리 입자

Schiller–Naumann 보정 종단속도와 1차 응답

js/world.js

검증

실행 중인 GLSL의 128² 부동소수점 readback과 실제 body 통계

js/renderer.js, js/controls.js

잎 윤곽

이 데모가 계산하는 것

5개 갈래, 호길이로 배치한 톱니, 잎자루

사실의 소유자

tools/leaf-model.mjs, shaders/leaf-outline.glsl

잎맥

이 데모가 계산하는 것

공간 식민화와 Murray 반지름 법칙으로 생성한 3개 망

사실의 소유자

tools/leaf-model.mjsshaders/leaf-venation.glsl

유리 재료

이 데모가 계산하는 것

SCHOTT N-BK7 Sellmeier 굴절률과 RGB 분산

사실의 소유자

js/glass.js

광학 표면

이 데모가 계산하는 것

타원체 캡, 잎맥 릿지, 기울기와 Hessian

사실의 소유자

shaders/leaf-thickness.glsl

낙하

이 데모가 계산하는 것

2D 타원판의 부가질량, Munk 모멘트, 준정상 양력·항력

사실의 소유자

js/dynamics.js

공기

이 데모가 계산하는 것

유선함수로 만든 6옥타브 발산 없는 속도장

사실의 소유자

js/air.js

유리 입자

이 데모가 계산하는 것

Schiller–Naumann 보정 종단속도와 1차 응답

사실의 소유자

js/world.js

검증

이 데모가 계산하는 것

실행 중인 GLSL의 128² 부동소수점 readback과 실제 body 통계

사실의 소유자

js/renderer.js, js/controls.js

GLSL은 shaders/만이 소유.
실제로 AI와 작업할때 둘이 합쳐서 작업하는 경우가 있어서 이 경우 명확히 분리하는게 좋았다. leaf model의 경우에 비전 OCR을 통해서 윤곽을 찍은 채로 전달해서 mjs 형태로 구현하는 형식을 취했다.

JavaScript 안에 셰이더 문자열이나 저품질 대체 셰이더를 두지 않는다. js/shaders.js는 명시적 파일 URL을 fetch하고, #include "..."를 한 프로그램당 한 번만 합성하게 했다. 파일이나 확장이 없으면 조용히 폴백하지 않고 시작 단계에서 실패하게 했다

Text
index.html
style.css
js/
  main.js          부팅, 프레임 루프, 시스템 배선
  shaders.js       명시적 GLSL 파일 로더와 include 해석기
  glass.js         N-BK7 Sellmeier와 파장별 굴절률
  dynamics.js      얇은 타원판의 강체 운동과 RK4
  air.js           발산 없는 난류 속도장
  world.js         잎·파편·입자 개체군과 측정 통계
  renderer.js      WebGL2 패스, 인스턴싱, GPU 감사
  controls.js      UI와 감사표
shaders/
  leaf-outline.glsl
  leaf-venation.glsl     생성 파일
  leaf-thickness.glsl
  background.frag
  leaf.frag
  particle.frag
  audit.frag
  post.frag
tools/
  leaf-model.mjs         윤곽·톱니·잎맥 생성기

ES module과 셰이더 fetch를 사용하므로 file://가 아니라 HTTP로 열게 해두었다.

런타임 자산 정책

  • 이미지, 외부 텍스처, 오디오, CDN JavaScript는 사용하지 않음

  • 배경, bokeh, 잎 표면과 유리 먼지는 모두 로컬 GLSL이 절차적으로 생성

  • 외부 자동 요청은 UI용 Google Fonts뿐이며 렌더링 결과에는 관여하지 않는다.

  • 모든 .vert, .frag, .glsljs/shaders.js의 명시적 new URL(..., import.meta.url) 매니페스트에 등록되는데. 이는 Makonea Lab이 iframe CSP 안에서 프로젝트 파일을 정적으로 식별하도록 하기 위한 조건

실제로 이러한 런타임 자산 정책을 통해서 샌드박스 블록에 격리하는데 성공했다. 개인적으로 구현이라는 것은 결국 올리고자 하는 대상의 상태에 따라서 바뀌니까.


2. 기준 상수와 기본 상태

아래 값은 비슷해 보이는 숫자를 파일마다 다시 적지 않는다. 공기 밀도와 중력은 dynamics.js에서 export하며 잎과 입자가 같은 값을 import하는 것을 체크함.

항목

기본값

단위 / 의미

공기 밀도 ρ_air

1.204

kg·m⁻³, 20 °C / 1 atm 모델값

중력가속도 g

9.80665

m·s⁻²

유리 밀도 ρ_glass

2500

kg·m⁻³, 동역학 모델값

공기 점도 μ

1.81×10⁻⁵

Pa·s

잎 반폭 a

0.070

m

공력 반두께 b

100

µm

화면의 물리 높이

0.60

m

난류 RMS

0.60

m·s⁻¹

난류 길이 척도

0.370–0.0149

m

광학 전파거리 d

0.20

잎 좌표

광학 돔 높이

0.17

잎 좌표

잎맥 relief

0.55 × 0.008

잎 좌표

주맥의 1/e 반폭 제곱

0.001

잎 좌표²

주변 입자 반지름

18–72

µm, 물리 계산용

파쇄 입자 반지름

28–143

µm, 물리 계산용

항목

공기 밀도 ρ_air

기본값

1.204

단위 / 의미

kg·m⁻³, 20 °C / 1 atm 모델값

항목

중력가속도 g

기본값

9.80665

단위 / 의미

m·s⁻²

항목

유리 밀도 ρ_glass

기본값

2500

단위 / 의미

kg·m⁻³, 동역학 모델값

항목

공기 점도 μ

기본값

1.81×10⁻⁵

단위 / 의미

Pa·s

항목

잎 반폭 a

기본값

0.070

단위 / 의미

m

항목

공력 반두께 b

기본값

100

단위 / 의미

µm

항목

화면의 물리 높이

기본값

0.60

단위 / 의미

m

항목

난류 RMS

기본값

0.60

단위 / 의미

m·s⁻¹

항목

난류 길이 척도

기본값

0.370–0.0149

단위 / 의미

m

항목

광학 전파거리 d

기본값

0.20

단위 / 의미

잎 좌표

항목

광학 돔 높이

기본값

0.17

단위 / 의미

잎 좌표

항목

잎맥 relief

기본값

0.55 × 0.008

단위 / 의미

잎 좌표

항목

주맥의 1/e 반폭 제곱

기본값

0.001

단위 / 의미

잎 좌표²

항목

주변 입자 반지름

기본값

18–72

단위 / 의미

µm, 물리 계산용

항목

파쇄 입자 반지름

기본값

28–143

단위 / 의미

µm, 물리 계산용

광학 두께와 공력 두께는 같은 값이 아니다.

기본 잎은 총 공력 두께가 200 µm인 판으로 떨어지지만, 화면의 광학 두께장은 약 0.072–0.174 잎 좌표를 차지. 70 mm 반폭에 그대로 환산하면 수 mm급 유리입니다. 200 µm 판은 낙엽처럼 움직일 수 있지만 화면에서 강한 코스틱을 거의 만들지 못하고, 수 mm 유리는 아름답게 굴절하지만 낙엽처럼 떨어지지 않는다


3. 단풍잎: 윤곽과 잎맥을 하나의 구조로 만들기

3.1 갈래 프로파일

각 갈래는 축 c, 길이 l, 각 반폭 w를 가진 함수

L(θ;c,l,w)=l[max(0,1θcw)]p.L(\theta;c,l,w)=l\left[\max\left(0,1-\frac{|\theta-c|}{w}\right)\right]^p.

넓은 어깨와 가는 첨두를 동시에 만들기 위해 갈래 하나당 두 프로파일의 최댓값을 사용합니다. 실제 코드도 식을 그대로 계산한다.

GLSL
float lobe(float th, float c, float len, float w) {
    return len * pow(max(0.0, 1.0 - abs(th - c) / w), LOBE_P);
}

float leafEnvelope(float th) {
    float r = 0.0;
    for (int i = 0; i < 5; i++) {
        r = max(r, lobe(th, LOBE_AXIS[i], LOBE_BL[i], LOBE_BW[i]));
        r = max(r, lobe(th, LOBE_AXIS[i], LOBE_SL[i], LOBE_SW[i]));
    }
    return r;
}

raised cosine처럼 축에서 미분값이 0인 프로파일은 끝을 둥글고 넓게 만든다. 현재의 선형 테이퍼 거듭제곱은 축에 모서리를 남겨 단풍잎 특유의 첨두를 보존

3.2 톱니는 각도 간격이 아니라 호길이 간격

극좌표 윤곽 r(θ)의 호길이는 다음과 같다.

s(θ)=θ0θr(ϕ)2+(drdϕ)2dϕ.s(\theta)=\int_{\theta_0}^{\theta} \sqrt{r(\phi)^2+\left(\frac{dr}{d\phi}\right)^2}\,d\phi.

생성기는 이 s를 등간격으로 나눠 톱니 꼭짓점을 놓더라, 이러한 구현이 신기하다. 각도를 등분하면 갈래 끝에서 톱니가 벌어지고 만입에서 뭉치기 때문인데, 두 꼭짓점 사이의 반지름은

r(u)=max(renv(1d),min((1u)r0+ur1,renv)[1dsinq(πu)])r(u)=\max\left(r_{env}(1-d), \min\left((1-u)r_0+ur_1, r_{env}\right) \left[1-d\sin^q(\pi u)\right]\right)

로 안쪽으로 파인다. q=TOOTH_SHARP>1이라 꼭짓점 사이에 V형 만입이 생기고, 갈래 축 가까이에서는 d를 0으로 보내 갈래 끝이 톱니 부채가 되는 것을 막는다.

GLSL
float u = (th - t0) / (t1 - t0);
float base = mix(TOOTH_R[lo], TOOTH_R[lo + 1], u);
float notch = pow(sin(PI * u), TOOTH_SHARP);
r = min(base * (1.0 - dip * notch), env);
r = max(r, floorR);

3.3 공간 식민화 잎맥

먼저 다섯 actinodromous 1차맥을 갈래 축에 놓는다. 그 뒤 잎몸 내부와 톱니 바로 안쪽에 공급원을 뿌린다 공급원은 영향반경 R_I=0.13 안에서 가장 가까운 마디를 끌어당기고, 새 마디가 R_K=0.045 안에 도달하면 소비, 성장 간격은 D=0.034 잎 좌표로 되어있다.

톱니 꼭짓점 자체가 공급원이므로 2차맥과 잔맥이 톱니를 향해 자람. 윤곽과 맥을 별개 장식으로 겹친 것이 아니라, craspedodromous 조건을 생성 과정에 넣은 것.

3.4 Murray 반지름 법칙

분기점의 맥 구경은 다음 보존식으로 뒤에서부터 계산

rp3=irci3.r_p^3=\sum_i r_{c_i}^3.
JavaScript
for (const i of order) {
    if (children[i].length === 0) { w[i] = 1; continue; }
    let s = 0;
    for (const c of children[i]) s += Math.pow(w[c], 3);
    w[i] = Math.pow(s, 1 / 3);
}

여기까지의 w는 생물학적 맥의 정규화 반지름인데, 화면에 그리는 가우시안 릿지는 가느다란 맥이 서브픽셀로 사라지지 않도록 최소 반지름 ε=0.065를 둔다.

rvis=ε+(1ε)w,σ2=σ02rvis2,h=h0w.r_{vis}=\varepsilon+(1-\varepsilon)w,\qquad \sigma^2=\sigma_0^2r_{vis}^2,\qquad h=h_0w.
GLSL
float radius = mix(VEIN_MIN_RADIUS, 1.0, w);
float widthSq = uVeinW * radius * radius;
float ridge = w * exp(-d * d / max(widthSq, 1e-7));

반원 단면 릿지라면 높이와 반폭 모두 반지름에 비례하므로 높이 지수는 1로 해두었다. 그전에는 w^2.35로 구현했으나 가장 가는 가지를 Murray 구경이 요구하는 것보다 훨씬 납작하게 만들었다.(실제로 보기 좋지 않았다) 별도의 미학적 높이 법칙을 쓰려면 Murray라는 이름 아래 섞지 않고 독립 파라미터로 명시해야 할것이다.

여러 선분은 max나 평범한 합 대신 6차 Lp 결합을 사용했다.

V(p)=(iGi(p)6)1/6.V(p)=\left(\sum_i G_i(p)^6\right)^{1/6}.
GLSL
float ridge2 = ridge * ridge;
union6 += ridge2 * ridge2 * ridge2;
return pow(max(union6, 1e-18), 1.0 / 6.0);

max는 승자가 바뀌는 곳에 곡률 주름을 만들고, 단순 합은 분기점의 공유 선분을 중복해 밝은 구슬을 만들어준다. L6 결합에서는 같은 높이의 두 선분이 만나도 높이가 2^(1/6)=1.122배에 머뭅니다.

3.5 생성 결과

leaf-venation.glsl의 헤더는 생성기가 직접 측정한 결과

항목

생성 결과

톱니

25개

잎맥 변형

3개

변형별 선분

210 / 195 / 211

총 선분

616, 예산으로 잘린 선분 없음

톱니 도달

세 변형 모두 25 / 25

평균 도달 오차

0.0379 / 0.0393 / 0.0429

윤곽 좌우 대칭 오차

1.70×10⁻⁴

잎몸 W/L

1.253

정규화 면적

0.6829

도심 x

0.32594

항목

톱니

생성 결과

25개

항목

잎맥 변형

생성 결과

3개

항목

변형별 선분

생성 결과

210 / 195 / 211

항목

총 선분

생성 결과

616, 예산으로 잘린 선분 없음

항목

톱니 도달

생성 결과

세 변형 모두 25 / 25

항목

평균 도달 오차

생성 결과

0.0379 / 0.0393 / 0.0429

항목

윤곽 좌우 대칭 오차

생성 결과

1.70×10⁻⁴

항목

잎몸 W/L

생성 결과

1.253

항목

정규화 면적

생성 결과

0.6829

항목

도심 x

생성 결과

0.32594

각 잎은 세 망 중 하나와 y축 반전 중 하나를 골라 여섯 가지 잎맥을 가진다.윤곽은 같은 종의 형태를 공유하고, 맥만 개체마다 달라진다.


4. N-BK7 분산: 수식에서 실행 코드까지

4.1 Sellmeier 식

파장 λ를 µm로 넣음

n2(λ)=1+i=13Biλ2λ2Ci.n^2(\lambda)=1+\sum_{i=1}^{3}\frac{B_i\lambda^2}{\lambda^2-C_i}.
JavaScript
const B = [1.03961212, 0.231792344, 1.01046945];
const C = [0.00600069867, 0.0200179144, 103.560653];

export function nBK7(um) {
    const l2 = um * um;
    let s = 1;
    for (let i = 0; i < 3; i++) s += (B[i] * l2) / (l2 - C[i]);
    return Math.sqrt(s);
}

계수에서 도출되는 값은 다음과 같다.

항목

계산값

기준값

n_d, 587.5618 nm

1.51680003

1.51680

Abbe 수 V_d

64.16734

64.17

수직입사 단일면 반사율

4.21646 %

((n−1)/(n+1))²

임계각

41.24519°

asin(1/n_d)

항목

n_d, 587.5618 nm

계산값

1.51680003

기준값

1.51680

항목

Abbe 수 V_d

계산값

64.16734

기준값

64.17

항목

수직입사 단일면 반사율

계산값

4.21646 %

기준값

((n−1)/(n+1))²

항목

임계각

계산값

41.24519°

기준값

asin(1/n_d)

렌더러는 대표 RGB 파장 650, 550, 450 nm를 샘플한다.

채널

n(λ)

(n−1)/(n_G−1)

R, 650 nm

1.51452031

0.992282

G, 550 nm

1.51852239

1.000000

B, 450 nm

1.52531950

1.013109

채널

R, 650 nm

n(λ)

1.51452031

(n−1)/(n_G−1)

0.992282

채널

G, 550 nm

n(λ)

1.51852239

(n−1)/(n_G−1)

1.000000

채널

B, 450 nm

n(λ)

1.52531950

(n−1)/(n_G−1)

1.013109

파장이 짧은 파랑이 가장 많이 꺾이고 빨강이 가장 적게 꺾이는데, 입자 셰이더도 같은 배율을 uniform으로 받아 사용한다.

GLSL
vec3 bend = (uIndex - 1.0) / (uIndex.g - 1.0);
vec3 bent = vec3(
    texture(uBg, vScreen + px * bend.r).r,
    texture(uBg, vScreen + px * bend.g).g,
    texture(uBg, vScreen + px * bend.b).b);

이전 입자 값 1.15 / 1.00 / 0.84는 분산의 폭을 과장한 것뿐 아니라 순서도 뒤집어 빨강을 가장 많이 이동시킴. 현재는 잎과 입자가 동일한 N-BK7 분산을 공유한다.


5. 두께장과 얇은 소자 광학

5.1 윤곽과 두께를 분리

광학 몸체는 직교좌표의 타원체 캡과 잎맥 릿지의 합이다.

D(p)=[max(0,1pca2)]0.60,D(p)=\left[\max\left(0,1-\left\|\frac{p-c}{a}\right\|^2\right)\right]^{0.60},
t(p)=hd[f+(1f)D(p)]+hvV(p)[0.25+0.75D(p)].t(p)=h_d\left[f+(1-f)D(p)\right] +h_vV(p)\left[0.25+0.75D(p)\right].
GLSL
float thickness(vec2 p, int variant) {
    float d = domeProfile(p);
    return uThick * (DOME_FLOOR + (1.0 - DOME_FLOOR) * d)
         + uVein * veinField(p, variant) * (0.25 + 0.75 * d);
}

두께장을 잎 윤곽으로 0에 잘라내지 않는다. 유한차분이 그 계단을 가로지르면 실제 표면 곡률이 아니라 잘라낸 경계의 무한 곡률을 읽기 때문이다. 윤곽은 마지막에 coverage와 스침각 Fresnel로 처리한다(이부분에 대해서는 왜 이렇게 했는지 아직도 모르겠다)

5.2 얇은 소자 사상

배경 평면까지의 전파거리를 d라 할 때, 얇은 두께장 t는 광선을

x=x+d(n1)t(x)x' = x+d(n-1)\nabla t(x)

로 이동시킨다. 이 사상의 야코비안은

J=I+d(n1)Ht,J=I+d(n-1)H_t,

이고, 한 갈래 근사에서 밝기 배율은

graw=1detJg_{raw}=\frac{1}{|\det J|}

이다. det J=0을 지나는 곳이 접힘과 코스틱 선으로 보인다.

5.3 기울기와 Hessian

중심, 축 방향 네 점, 대각선 네 점의 총 9개 표본으로 기울기와 Hessian을 함께 계산한다.

GLSL
float txx = (px - 2.0 * t0 + mx) / (e * e);
float tyy = (py - 2.0 * t0 + my) / (e * e);
float txy = (pp - pm - mp + mm) / (4.0 * e * e);

j.grad = vec2(px - mx, py - my) / (2.0 * e);
j.detJ = (1.0 + k * txx) * (1.0 + k * tyy)
       - (k * txy) * (k * txy);

과거 구현은 생 2차 차분이 이미 포함한 를 다시 곱해 Hessian 효과를 e²=3.6×10⁻⁵만큼 줄였다. 반대로 윤곽 자체를 돔 반지름으로 사용한 시도는 톱니와 만입의 고주파 곡률을 모두 두께에 넣어 넓은 면적을 접었는데, 현재는 Hessian 단위와 두께장의 매끄러움을 각각 바로잡았다.

5.4 화면 표시용 코스틱

1/|det J|는 접힘에서 발산하고, 현재 렌더러는 여러 원상을 모아 적분하지 않는다. 그러므로 원시 determinant는 감사표에 보존하되 화면에는 로그 압축한 한 갈래 이득을 사용한다.

=clamp{ln[max(detJ,0.08)],0.72,0.72},\ell=\operatorname{clamp}\{-\ln[\max(|\det J|,0.08)],-0.72,0.72\},
gc=e0.58,gdisplay=(1c)+cgc(1c)+cgcleaf.g_c=e^{0.58\ell},\qquad g_{display}=\frac{(1-c)+c g_c}{\langle(1-c)+c g_c\rangle_{leaf}}.
GLSL
float logGain = -log(max(abs(detJ), 0.08));
return exp(0.58 * clamp(logGain, -0.72, 0.72));

분모는 CPU에서 비슷하게 다시 계산한 값이 아니라. audit.frag가 같은 thicknessJet()을 128² RGBA32F target에 렌더한 뒤 readPixels로 측정한 잎 내부 평균이다.(계산을 좀 더 빠르게 하기위해서)

5.5 Fresnel, Beer–Lambert, 3D 자세

표면 법선은 앞면 z=t/2의 기울기로 만든다.

nlocal=normalize(12tx,12ty,1).n_{local}=\operatorname{normalize}(-\tfrac12t_x,-\tfrac12t_y,1).

이 법선을 적분된 잎의 tilt와 화면 spin으로 회전하는데, 실루엣만 납작하게 만들고 법선을 정면에 두는 2D 트릭을 사용하지 않는다.

비편광 Fresnel은 s, p 편광 반사율의 평균이다.

F=12[(n1cosθin2cosθtn1cosθi+n2cosθt)2+(n2cosθin1cosθtn2cosθi+n1cosθt)2].F=\frac12\left[ \left(\frac{n_1\cos\theta_i-n_2\cos\theta_t} {n_1\cos\theta_i+n_2\cos\theta_t}\right)^2+ \left(\frac{n_2\cos\theta_i-n_1\cos\theta_t} {n_2\cos\theta_i+n_1\cos\theta_t}\right)^2 \right].

투과광은 Beer–Lambert 형식 T=exp(−σL)을 사용한다.. 다만 uTint와 채널별 흡수 배율은 SCHOTT의 분광 흡수 데이터가 아니라 시각적 색조 파라미터이다.. 굴절률과 Fresnel은 재료 데이터에 묶여 있지만 흡수색까지 물성값이라고 주장하지 않는다.


6. 낙하: 얇은 타원판의 준정상 모델

6.1 질량, 관성, 부가질량

반폭 a, 반두께 b인 2D 타원판을 단위 깊이로 해석

m=πabρg,I=14m(a2+b2),m=\pi ab\rho_g,\qquad I=\frac14m(a^2+b^2),
m11=ρaπb2,m22=ρaπa2,m_{11}=\rho_a\pi b^2,\qquad m_{22}=\rho_a\pi a^2,
Ia=18ρaπ(a2b2)2.I_a=\frac18\rho_a\pi(a^2-b^2)^2.

기본 잎에서 I*=0.5824, broadside 힘평형 속도는 1.797 m/s이다.

6.2 힘과 Munk 모멘트

몸체 좌표의 상대속도를 (u,v), 받음각을 α=atan2(v,u)라 두었다.

CD=CD0+(CD90CD0)sin2α,CL=CL0sin2α.C_D=C_{D0}+(C_{D90}-C_{D0})\sin^2\alpha, \qquad C_L=C_{L0}\sin 2\alpha.

Munk 모멘트의 부호가 중요하다.

τM=(m22m11)uv.\tau_M=-(m_{22}-m_{11})uv.
JavaScript
const CD = P.CD0 + (P.CD90 - P.CD0) * sa * sa;
const CL = P.CL0 * Math.sin(2 * al);
const q = 0.5 * RHO_AIR * V * V * 2 * P.a;
const Fu = -q * (CD * ca - CL * sa);
const Fv = -q * (CD * sa + CL * ca);

const tau = -(P.m22 - P.m11) * u * v
          + P.kcp * P.a * ca * Fv
          - P.Ctau * RHO_AIR * P.a ** 4 * Math.abs(w) * w;

상태 s=[x,y,θ,u,v,ω]는 한 프레임을 네 하위 구간으로 나눠 RK4로 적분하는데, 화면의 out-of-plane tilt는 별도 사인파가 아니라 이 θ이다.

6.3 구현하지 못한것

준정상 C_L(α), C_D(α)에는 뒤에 와류를 구현하지 못했다. 이부분에 대해서는 추후 논문을 찾아서 추가로 도전해 볼 것이다.


7. 공기: 발산 없는 다중척도 속도장

2D 유선함수의 옥타브 합을

ψ(x,y,t)=jAjsin(kxjx+kyjy+ωjt+ϕj)\psi(x,y,t)=\sum_j A_j\sin(k_{xj}x+k_{yj}y+\omega_jt+\phi_j)

로 두고,

u=ψy,v=ψxu=\frac{\partial\psi}{\partial y},\qquad v=-\frac{\partial\psi}{\partial x}

로 속도를 만든다. 따라서 ∂u/∂x+∂v/∂y=0이 구성상 성립한다. Kolmogorov 구조함수 δv∝ℓ^(1/3)에 맞춰 속도 진폭이 k^(−1/3), 유선함수 진폭이 k^(−4/3)이 되도록 두는데,

JavaScript
const c = Math.cos(g.kx * x + g.ky * y + g.om * t + g.ph) * g.amp;
u += c * g.ky;
v += -c * g.kx;

6개 옥타브의 가장 큰 길이 척도는 0.3696 m, 가장 작은 척도는 0.01493 m이다. 0.60 m 화면보다 큰 단일 와류가 모든 잎을 같은 방향으로 미는 현상을 피하도록 적분척도를 0.37 m로 낮췄다. 무작위 위상 합은 진폭합이 아니라 RMS로 정규화하며, 실행 중 재측정값은 0.999이다.


8. 유리 입자와 파쇄 먼지

8.1 Stokes 범위 밖을 다루는 방법

구형 입자의 Stokes 종단속도는

vS=2(ρpρa)gr29μv_S=\frac{2(\rho_p-\rho_a)gr^2}{9\mu}

이지만, 큰 파쇄 입자는 Re<1에 머물지 않는다. 현재는 속도를 임의 상한으로 자르지 않고 Schiller–Naumann 보정을 반복해 힘평형을 푼다.

Re=2ρarvμ,CD=24Re(1+0.15Re0.687).Re=\frac{2\rho_a r v}{\mu},\qquad C_D=\frac{24}{Re}\left(1+0.15Re^{0.687}\right).

Stokes 속도에 대한 보정형은

v=vS1+0.15Re0.687v=\frac{v_S}{1+0.15Re^{0.687}}

이고 Rev에 의존하므로 10회 고정점 반복한다.

JavaScript
for (let i = 0; i < 10; i++) {
    reynolds = Math.max(1e-8,
        (2 * RHO_AIR * radius * settle) / AIR_VISCOSITY);
    correction = 1 + 0.15 * reynolds ** 0.687;
    settle = vStokes / correction;
}

현재 반지름 범위의 계산 결과는 다음과 같다.

반지름

보정 종단속도

Reynolds 수

18 µm

0.093 m/s

0.22

72 µm

0.931 m/s

8.92

28 µm

0.209 m/s

0.78

143 µm

2.121 m/s

40.30

반지름

18 µm

보정 종단속도

0.093 m/s

Reynolds 수

0.22

반지름

72 µm

보정 종단속도

0.931 m/s

Reynolds 수

8.92

반지름

28 µm

보정 종단속도

0.209 m/s

Reynolds 수

0.78

반지름

143 µm

보정 종단속도

2.121 m/s

Reynolds 수

40.30

입자 속도는 dv/dt=(u_air−v)/τ의 한 프레임 정확해 1−exp(−Δt/τ)로 갱신하므로 τ<Δt인 작은 입자도 수치적으로 폭주하지 않는다

8.2 물리 크기와 보이는 크기

18–143 µm 입자는 0.60 m 화면에서 거의 보이지 않는다. 따라서 물리 반지름은 항력과 종단속도에만 사용하고, 렌더링 반크기는 주변 입자 0.45–1.45 mm, 파쇄 입자 0.70–3.10 mm로 확대한다.(그냥 눈에 보이는 것만 눈에 보이는 것만 작업한 것이지 실제 비례 크기는 아닌것으로 보인다)

입자 모양은 원 하나가 아니라 3차·5차 각주기 변형을 가진 작은 facet이며, 배경을 N-BK7 파장 비율로 다시 읽어 분산을 만든다. 잎 전체의 616개 선분 두께장을 작은 입자마다 평가하지 않는 별도 저비용 셰이더이다.


9. 파쇄 모델

유리 파편은 정갈한 삼각형이 아니라 잎 도심 주변에 뿌린 12개 seed의 Voronoi 셀로 만든다.

Ci={p:psipsj, ji}.C_i=\{p:\|p-s_i\|\leq\|p-s_j\|,\ \forall j\neq i\}.
GLSL
float shardMask(vec2 p, float seed, float which) {
    float dm = length(p - shardSeed(which, seed));
    float best = 1e9;
    for (int i = 0; i < 12; i++) {
        if (float(i) == which) continue;
        best = min(best, length(p - shardSeed(float(i), seed)));
    }
    return best - dm;
}

셀의 위치, 파쇄 시점, 파열 impulse는 시각 모델인데, 균열 선단, 응력확대계수, 파괴에너지나 충돌 접촉을 푸는 fracture mechanics는 아니다


10. 실행 중 감사

10.1 GPU 광학 감사

audit.frag는 렌더링 셰이더와 같은 leaf-thickness.glsl을 include합니다. 128² RGBA32F target에서 thicknessJet()을 직접 실행해 다음을 읽었다.

  • det(I+d(n−1)H) 최솟값·최댓값

  • det<0인 접힌 면적

  • |det|<0.15인 코스틱 선 면적

  • 중앙 편향 d(n−1)|∇t|

  • 표시 이득의 잎 내부 평균

  • 두께 최솟값·최댓값

2026-08-18, 기본 설정, 크롬 브라우저에서 측정한 값은 다음과 같은데, 이 표는 코드 상수가 아니라 회귀 확인을 위한 관측 기록이다.

항목

관측값

determinant 범위

−1.04 … 6.32

접힌 잎 면적

9.9 %

코스틱 선 면적

11.3 %

중앙 편향 중앙값

0.0112 잎 좌표

정규화 전 표시 이득

1.059

적용 정규화

×0.944

두께 범위

0.072–0.174 잎 좌표

렌더링

60 FPS 관측, 콘솔 오류 없음

항목

determinant 범위

관측값

−1.04 … 6.32

항목

접힌 잎 면적

관측값

9.9 %

항목

코스틱 선 면적

관측값

11.3 %

항목

중앙 편향 중앙값

관측값

0.0112 잎 좌표

항목

정규화 전 표시 이득

관측값

1.059

항목

적용 정규화

관측값

×0.944

항목

두께 범위

관측값

0.072–0.174 잎 좌표

항목

렌더링

관측값

60 FPS 관측, 콘솔 오류 없음

FPS는 장치·창 크기에 따라 달라진다(따라서 반드시 이 값에 일치한다고 할 수 없다)

10.2 낙하 통계는 하드코딩하지 않음

각 잎의 실제 적분 속도를 월드 좌표로 되돌려 수직선과의 순간 각도를 측정하였다.

다만 이 부분은 추후에 하드코딩하는게 성능면에서 유리한것으로 보인다.

γinst=tan1vxvy,vy<0.\gamma_{inst}=\tan^{-1}\frac{|v_x|}{-v_y},\qquad v_y<0.

감사표는 최근 최대 2048개 표본의 중앙값을 보여준다. 동시에 통계 reset 이후 모든 잎의 순 변위를 합쳐

γnet=tan1ΔxΔy\gamma_{net}=\tan^{-1}\frac{|\sum\Delta x|}{|\sum\Delta y|}

도 표시한다. gust 슬라이더를 바꾸면 이전 공기장에서 얻은 표본을 섞지 않도록 통계를 초기화한다.


11. 수정 이력: 잘못된 구현에서 현재 구현으로

문제

왜 틀렸는가

현재 구현

입자 RGB 1.15/1/0.84

빨강을 더 꺾고 파랑을 덜 꺾어 N-BK7 분산 순서를 역전

n(λ)−1의 실제 비 0.9923/1/1.0131

Stokes 속도를 0.65/1.35 m/s에서 clamp

그 속도에서 이미 Re≈4–12; 저Re 법칙의 범위 밖

Schiller–Naumann을 속도와 함께 반복 계산

잎맥 높이 w^2.35

반원 릿지의 높이는 반지름과 선형; Murray 아래에 두면 오해

높이와 반폭 모두 w에 선형, 가시성 최소폭은 별도 명시

ρ, g가 파일마다 다름

잎과 먼지가 같은 화면에서 다른 공기를 통과

dynamics.js의 상수를 world.js가 import

낙하 각도 두 줄이 상수이고 predicate가 true

난류척도 변경을 절대 검출하지 못함

실제 적분 body의 순간 중앙값과 순변위를 런타임 측정

Hessian에 가 두 번 반영

determinant가 거의 1이라 코스틱이 사실상 꺼짐

생 2차 차분을 정확히 로 나눔

윤곽으로 돔을 나눔

톱니·만입의 곡률이 두께장에 들어가 넓은 면적이 접힘

독립된 직교 타원체 캡

잎맥 결합에 max 또는 단순 합

교차선의 주름 또는 분기점의 밝은 구슬

매끄러운 L6 union

잎좌표 편향을 UV에 바로 더함

창 크기에 따라 굴절 세기가 달라짐

tilt → spin → scale → aspect 순으로 변환

실루엣과 법선의 자세가 분리

edge-on 잎도 정면 Fresnel을 유지

적분 자세로 실루엣과 법선을 함께 회전

0.80 m 적분척도

0.60 m 화면에서 개체군이 한 덩어리로 이동

0.37 m부터 0.0149 m까지 6옥타브

문제

입자 RGB 1.15/1/0.84

왜 틀렸는가

빨강을 더 꺾고 파랑을 덜 꺾어 N-BK7 분산 순서를 역전

현재 구현

n(λ)−1의 실제 비 0.9923/1/1.0131

문제

Stokes 속도를 0.65/1.35 m/s에서 clamp

왜 틀렸는가

그 속도에서 이미 Re≈4–12; 저Re 법칙의 범위 밖

현재 구현

Schiller–Naumann을 속도와 함께 반복 계산

문제

잎맥 높이 w^2.35

왜 틀렸는가

반원 릿지의 높이는 반지름과 선형; Murray 아래에 두면 오해

현재 구현

높이와 반폭 모두 w에 선형, 가시성 최소폭은 별도 명시

문제

ρ, g가 파일마다 다름

왜 틀렸는가

잎과 먼지가 같은 화면에서 다른 공기를 통과

현재 구현

dynamics.js의 상수를 world.js가 import

문제

낙하 각도 두 줄이 상수이고 predicate가 true

왜 틀렸는가

난류척도 변경을 절대 검출하지 못함

현재 구현

실제 적분 body의 순간 중앙값과 순변위를 런타임 측정

문제

Hessian에 가 두 번 반영

왜 틀렸는가

determinant가 거의 1이라 코스틱이 사실상 꺼짐

현재 구현

생 2차 차분을 정확히 로 나눔

문제

윤곽으로 돔을 나눔

왜 틀렸는가

톱니·만입의 곡률이 두께장에 들어가 넓은 면적이 접힘

현재 구현

독립된 직교 타원체 캡

문제

잎맥 결합에 max 또는 단순 합

왜 틀렸는가

교차선의 주름 또는 분기점의 밝은 구슬

현재 구현

매끄러운 L6 union

문제

잎좌표 편향을 UV에 바로 더함

왜 틀렸는가

창 크기에 따라 굴절 세기가 달라짐

현재 구현

tilt → spin → scale → aspect 순으로 변환

문제

실루엣과 법선의 자세가 분리

왜 틀렸는가

edge-on 잎도 정면 Fresnel을 유지

현재 구현

적분 자세로 실루엣과 법선을 함께 회전

문제

0.80 m 적분척도

왜 틀렸는가

0.60 m 화면에서 개체군이 한 덩어리로 이동

현재 구현

0.37 m부터 0.0149 m까지 6옥타브


실제로 AI로 작업하면서 구현에서 게산이 실제로 틀린 경우가 있었다. 처음 한번에 구현을 못해서, 그 구현들에 대해서 잘못된 값을 측정해서 기록해두었다.

AI는 나보다 훨씬 똑똑하지만, 내가 원하는대로 한번에 하지는 못하는 것 같다. 내 생각에 논문을 넣고 수식을 넣었을때 중간 값이 증발하는 문제로 발생하는 것 같다.

12. 렌더링 파이프라인

Text
background.frag → RGBA16F background
        │
        ├─ copy.frag → RGBA16F scene
        │
        ├─ leaf.frag
        │    ├─ N-BK7 RGB 굴절
        │    ├─ Fresnel + Beer–Lambert
        │    └─ GPU 정규화 코스틱
        │
        ├─ particle.frag → 유리 먼지와 파쇄 glint
        │
        └─ post.frag → exposure / tone mapping → 화면

audit.frag → RGBA32F 128² → readPixels → 감사표와 caustic 정규화

잎과 파편은 instanced quad이다. 멀리 있는 작은 개체를 먼저 그린 뒤 가까운 개체가 덮는다. 배경은 화면공간 텍스처이므로 겹친 잎이 서로를 다시 굴절시키지는 않는다.


13. 조작

컨트롤

의미

Leaves

잎 개체 수, 1–40

Leaf half-span

실제 동역학 반폭, 20–120 mm

Half-thickness

공력 반두께, 60–1200 µm; I*와 운동영역 변경

Gust speed

RMS 난류 속도, 0–3.0 m/s; 변경 시 운동 통계 reset

Propagation distance

얇은 소자 전파거리 d, 0–1.5 잎 좌표

Caustic

압축 코스틱 이득 혼합량

Dome

광학 타원체 캡 높이

Vein relief

생성 잎맥의 광학 높이

Glass motes

주변 입자와 파쇄 먼지 밀도

Absorption

예술적 Beer–Lambert 색 흡수량

Exposure

최종 HDR 노출

Time

관찰용 시간 배율, 0.03–2.0×

컨트롤

Leaves

의미

잎 개체 수, 1–40

컨트롤

Leaf half-span

의미

실제 동역학 반폭, 20–120 mm

컨트롤

Half-thickness

의미

공력 반두께, 60–1200 µm; I*와 운동영역 변경

컨트롤

Gust speed

의미

RMS 난류 속도, 0–3.0 m/s; 변경 시 운동 통계 reset

컨트롤

Propagation distance

의미

얇은 소자 전파거리 d, 0–1.5 잎 좌표

컨트롤

Caustic

의미

압축 코스틱 이득 혼합량

컨트롤

Dome

의미

광학 타원체 캡 높이

컨트롤

Vein relief

의미

생성 잎맥의 광학 높이

컨트롤

Glass motes

의미

주변 입자와 파쇄 먼지 밀도

컨트롤

Absorption

의미

예술적 Beer–Lambert 색 흡수량

컨트롤

Exposure

의미

최종 HDR 노출

컨트롤

Time

의미

관찰용 시간 배율, 0.03–2.0×

패널은 기본으로 접혀 있고 inert 상태로 두었다(makonea 썸네일 문제)버튼으로 열면 수식의 파라미터와 현재 감사 결과를 함께 볼 수 있다.


14. 알려진 한계

  • 얇은 소자와 한 갈래 화면공간 광학입니다. 접힌 영역의 여러 원상을 경로추적하지 않는다.

  • 유리 내부의 횡방향 이동, 다중 내부반사, 편광 간섭을 계산하지 않는다.

  • uTint 흡수는 N-BK7의 측정 분광 투과율이 아니라 디자인 파라미터이다.

  • 겹친 잎은 서로를 굴절하지 않습니다. 모든 잎은 같은 원본 배경을 읽는다.

  • 광학 두께와 공력 두께가 의도적으로 분리되어 있다.

  • 공력은 2D 준정상 모델이며 비정상 후류, vortex shedding, 3D precession이 없다.

  • 공기장은 발산 없는 합성장이지 Navier–Stokes DNS가 아니다.

  • Schiller–Naumann 입자는 구형·고립 입자 근사이며 유리 조각의 실제 비구형 항력이 아니다.

  • 입자의 표시 크기는 픽셀 가시성을 위해 물리 크기보다 크다(실제로 입자를 좀 더 작게하니까 보기에 좋지 않았다)

  • 파쇄는 Voronoi 시각 모델이고 응력 기반 파괴해석이 아니다.

  • 잎맥은 고정 seed 세 개와 반전으로 여섯 종류뿐이므로 많은 잎에서는 반복된다.

  • 공간 식민화는 형태 발생 알고리즘이지 auxin 수송 PDE의 직접 해가 아니다.

  • WebGL2와 EXT_color_buffer_float, HTTP 제공 환경이 필요하다


16. 참고문헌

  1. SCHOTT, Optical Glass Datasheet — N-BK7.

  2. L. J. Hickey, “Classification of the Architecture of Dicotyledonous Leaves,” American Journal of Botany 60 (1973), 17–33.

  3. A. Runions et al., “Modeling and visualization of leaf venation patterns,” ACM Transactions on Graphics 24 (2005), 702–711, DOI: 10.1145/1073204.1073251.

  4. C. D. Murray, “The Physiological Principle of Minimum Work,” Journal of General Physiology 9 (1926), 835–841.

  5. M. Born and E. Wolf, Principles of Optics, 7th ed., Cambridge University Press, 1999.

  6. H. Lamb, Hydrodynamics, 6th ed., Cambridge University Press, 1932, §71.

  7. M. M. Munk, The Aerodynamic Forces on Airship Hulls, NACA Report 184 (1924).

  8. A. Andersen, U. Pesavento and Z. J. Wang, “Unsteady aerodynamics of fluttering and tumbling plates,” Journal of Fluid Mechanics 541 (2005), 65–90.

  9. S. B. Field et al., “Chaotic dynamics of falling disks,” Nature 388 (1997), 252–254.

  10. L. Schiller and A. Naumann, “A drag coefficient correlation,” Zeitschrift des Vereines Deutscher Ingenieure 77 (1935), 318–320.


개인적인 메모: 세상 정말 좋아졌다. 그냥 비슷한 논문들 찾아서 검색한 후에 각각 논문들의 값만 넣어주기만 하면 애니메이션이 뽑힌다. 내가 원하는 것과 비슷하다. 잎맥쪽 부분에서 계산 공식을 좀 더 고치면 좋을것 같다.