09

Part III · Compute Core Chapter 09 of 18 Build 2025.31550

Noise와 Displacement

“같은 noise 함수가 fragment shader에서는 픽셀을, compute POP에서는 점을 움직인다. 영역만 다르고 식은 같다.”

이 챕터에서 정복할 CG 개념

왜 이게 중요한가

POP은 GPU 그래픽스 파이프라인의 데이터 모델을 노드 그래프로 노출시킨 학습 환경이다. 그 데이터 모델에서 가장 강력한 단일 도구가 noise다. The Book of Shaders와 Inigo Quilez의 articles 페이지에서 fragment shader 학습자가 배우는 noise는 vec2 uv를 받아 float를 반환한다. GLSL POP에서 같은 noise는 vec3 P를 받아 vec3 변위를 만든다. 함수는 같고 영역만 다르다. 이 추상화가 한번 잡히면 procedural terrain, vertex displacement, force field, advection이 모두 noise 한 번 호출의 변주임이 보인다. RTR4 §17 "Volume Rendering"부터 §13 "Beyond Polygons"까지가 모두 이 한 가지 도구의 변주다.

POP에서의 노출 지점

Noise POP (노드 버전)

GLSL POP의 TDSimplexNoise() (내장 함수)

GLSL POP의 GLSL 페이지에 simplexnoise 메뉴 파라미터가 있다.

코드 측에서는 TDSimplexNoise(...)를 직접 호출 가능. 인자 차원 (vec2 / vec3 / vec4) 및 반환 타입(float vs vec)의 오버로드는 wiki 미확정 — 실험 필요. 본 챕터의 코드는 vec3 입력에 대한 가장 보편적인 형태로 작성한다.

simplexnoise performance vs quality

이론

Noise가 왜 절차적 그래픽스의 중심인가

자연 표면 / 자연 운동의 시각적 본질은 임의성 + 자기상관이다. 완전한 random은 시각적으로 거칠다 (white noise). 완전한 규칙은 인공적이다 (sin wave). 자연은 그 사이 — 가까운 점끼리는 비슷하고 멀어질수록 독립인 신호. 그것이 noise다.

procedural art / generative graphics / scientific visualization의 거의 모든 자연스러운 패턴이 noise의 변주다:

이 모든 시각적 효과가 같은 함수의 다른 평가 방식에서 나온다는 사실이 noise의 진짜 힘이다.

Noise 분류 (3분 요약)

종류 입력 격자 외삽 비용 대표 사용처
value noise vec_n 정규 격자 격자 코너의 random scalar 보간 가장 쌈 빠른 패턴
gradient (Perlin) noise vec_n 정규 격자 격자 코너의 random gradient로 dot product 중간 가장 보편
simplex noise vec_n simplex 격자 (n+1 vertex) 더 적은 보간 횟수 중간 (저차원에서 더 쌈) 고차원에서 유리
cellular (Worley) noise vec_n 점 분포 가장 가까운 점까지의 거리 중간~비쌈 셀, 균열

simplex는 Ken Perlin이 Perlin noise 이후에 발표한 후속이다. 같은 차원에서 보간이 더 적고, 차원이 늘어날 때 비용이 (n+1)차원 격자가 아니라 n+1 vertex의 simplex로 줄어든다. 4D 이상에서 차이가 크다. 3D에서는 차이가 미미하지만 TouchDesigner가 simplex를 채택한 것은 확장성 측면에서 자연스럽다.

Simplex가 Perlin을 대체한 이유 — 차원의 저주

Ken Perlin의 1985년 gradient noise는 좌표 격자를 (n+1)차원 hypercube로 나눈다. 한 점에서 noise를 평가하려면 hypercube의 2ⁿ개 corner gradient를 보간한다. 3D에서는 8 corner, 4D에서는 16 corner.

Simplex noise (2001, Perlin 본인)는 격자를 정삼각형(simplex)으로 나눈다. n차원에서 simplex는 (n+1) 점으로 정의된다. 3D에서는 4 점, 4D에서는 5 점. 보간 비용이 hypercube의 2ⁿ에서 simplex의 (n+1)로 줄어든다.

차원이 늘면 차이가 더 벌어진다. 4D 이상의 noise (시간 차원 추가)는 simplex의 효율 이득이 크다. 또한 simplex는 격자 축에 대한 anisotropy가 hypercube보다 약해 시각적으로 더 isotropic하다.

TouchDesigner가 TDSimplexNoise()로 simplex를 채택한 것은 자연스럽다. 3D에서도 simplex가 약간 더 빠르고, 4D 시간-noise로 확장할 때 확실히 유리하다.

같은 함수, 다른 영역

fragment shader (canvas 한 면):

// fragment shader
in vec2 vUV;
void main() {
    float n = noise(vUV * 4.0);
    FragColor = vec4(vec3(n), 1.0);
}

GLSL POP (3D 점 구름):

// GLSL POP compute shader
void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;
    vec3 p = TDIn_P();
    float n = TDSimplexNoise(p * 1.5);  // 영역만 vec3로 바뀜
    p += normalize(p) * n * 0.3;
    P[id] = p;
}

같은 noise. fragment에서는 (u, v) 평면 위에서 모든 픽셀이 동시에 noise 한 번. POP에서는 (x, y, z) 공간 위에서 모든 point가 동시에 noise 한 번. dispatch 모델이 1차원이냐(POP) 2차원이냐(fragment)의 차이는 inside 함수에는 보이지 않는다.

Noise 함수의 수학적 요구사항

좋은 noise 함수가 갖춰야 할 성질:

  1. 결정성 (deterministic): 같은 입력 좌표는 항상 같은 출력. seed가 고정되어야 한다.
  2. 연속성 (continuity): 좌표가 미세하게 변하면 출력도 미세하게 변한다. value noise는 lattice 경계에서 C¹ 불연속, gradient noise는 C¹ 연속.
  3. 자기상관 감소 (decorrelation): 좌표 거리가 1을 넘으면 출력 상관이 0에 가까움. 패턴이 반복되지 않는다.
  4. 무방향성 (isotropy): 어떤 축 방향으로 회전해도 통계적 성질이 같다. 격자 기반 noise는 격자 축에 약한 anisotropy가 있다. simplex noise는 simplex 격자로 이 약점을 줄임.
  5. 균등 분포 (uniformity): 출력 값의 히스토그램이 균등에 가깝다. fBm은 정규분포에 수렴.

CG에서 noise는 보통 [-1, 1] 또는 [0, 1] 범위로 normalize 한다. simplex noise의 출력 범위는 구현마다 약간 다르고, TD의 TDSimplexNoise() 정확한 범위는 wiki에 명시되지 않음 — wiki 미확정 — 실험 필요. 안전하게는 출력에 * 0.5 + 0.5 또는 clamp을 더해 명시적으로 범위를 맞춘다.

Displacement: scalar를 vector로

noise는 보통 float (혹은 vec)를 반환한다. point P를 움직이려면 vec3 변위가 필요하다. 세 가지 표준 패턴:

  1. Normal 방향 displacement (Bump-style):

    p += N * noise(p);   // N은 surface normal

    sphere를 surface noise로 부풀린다. 부드럽고 직관적. 그러나 self-intersection 가능.

  2. Position-direction displacement (radial):

    p += normalize(p) * noise(p);   // 원점 방향 = 가짜 normal

    sphere처럼 원점에서 뻗어나가는 형태에 적합. normal attribute 없이도 가능.

  3. 3-noise vector displacement (3D vector field):

    p += vec3(noise(p+a), noise(p+b), noise(p+c)) * 0.3;

    각 축에 독립 noise. 자연스럽지만 divergence가 있어 점들이 한쪽으로 모인다.

Noise의 비용 — 한 호출이 몇 ALU인가

Simplex noise의 단일 호출은 구현에 따라 30–80 ALU 명령 정도 한다. 격자 좌표 계산, hash, gradient 평가, 보간이 들어간다. value noise는 더 가볍고 (20–40), 잘 구현된 fBm 5 octave는 ~250 ALU.

GPU의 ALU throughput은 RTX 3070 기준 약 20 TFLOPS = 20조 ALU/초. noise 100,000회/frame은 100,000 × 80 = 8M ALU. 60 fps에서 GPU 사용률은 0.024%. 즉 noise 자체가 병목인 경우는 드물다.

병목은 보통 다른 데서 온다 — SSBO read/write bandwidth, register pressure, branch divergence. noise를 줄이는 최적화는 보통 보상이 작다.

다만 fBm 8 octave가 SSBO read와 같은 step에서 일어나면 register pressure로 occupancy가 떨어질 수 있다 — 그때만 noise 호출 패턴을 재검토.

Curl Noise — divergence-free 벡터장

curl noise는 점들이 한쪽으로 뭉치지 않게 흐르는 벡터장이다. fluid 시뮬레이션의 incompressibility 조건 (div(v) = 0)을 충족한다.

수학적으로 2D에서 curl noise는 scalar potential ψ의 회전:

v = (∂ψ/∂y, -∂ψ/∂x)

3D에서는 vector potential Ψ = (ψ₁, ψ₂, ψ₃)의 curl:

v = ∇ × Ψ

미분을 직접 계산하기는 어렵고, Inigo Quilez 식의 유한 차분 근사가 표준이다:

vec3 curlNoise(vec3 p) {
    const float eps = 0.01;
    // 각 성분에 대해 인접 두 noise의 차로 partial derivative 근사
    float n1 = TDSimplexNoise(p + vec3(0,   eps, 0));
    float n2 = TDSimplexNoise(p - vec3(0,   eps, 0));
    float n3 = TDSimplexNoise(p + vec3(0,   0, eps));
    float n4 = TDSimplexNoise(p - vec3(0,   0, eps));
    float n5 = TDSimplexNoise(p + vec3(eps, 0,   0));
    float n6 = TDSimplexNoise(p - vec3(eps, 0,   0));
    float a = (n1 - n2) / (2.0 * eps);
    float b = (n3 - n4) / (2.0 * eps);
    float c = (n5 - n6) / (2.0 * eps);
    return vec3(b - a, c - b, a - c);  // 3D curl 근사 (벡터 potential은 단일 noise 재사용)
}

이 코드가 GPU에서 실제로 하는 일: 한 점 p에서 noise를 6번 호출하여 세 축 방향의 편미분을 유한 차분으로 근사한다. 이 편미분들을 적절히 조합한 것이 curl이며, 결과 벡터는 divergence가 0에 가깝다. 따라서 이 벡터장에 따라 점을 움직이면 점 밀도가 시간이 지나도 보존된다 — fluid 같은 운동.

본격적인 3D curl은 세 개의 별도 potential noise가 필요하다. 위 코드는 한 noise를 세 축에서 sampling해 근사한 것 — Inigo Quilez 글의 단순화 버전이다.

Ridge noise — 절댓값 트릭

simplex noise는 출력이 [-A, A] 범위의 양음수다. fBm의 옥타브 누적에서 노이즈에 절댓값을 씌우면 새로운 시각적 효과가 나온다.

float ridge = 1.0 - abs(TDSimplexNoise(p));   // 0..1 범위, 능선이 더 도드라짐

여러 옥타브를 ridge로 누적하면 산악 능선과 비슷한 fractal 외형이 만들어진다. 자연 지형에 가장 가까운 noise 변형. 빌리어드 noise / billow noise도 비슷한 변형 (절댓값으로 골짜기를 도드라지게).

Domain warping — f(g(p)) 패턴

curl보다 더 강력한 단일 트릭이 domain warping이다. noise의 입력 자체를 noise로 한 번 더 비튼다.

vec3 warp = vec3(
    TDSimplexNoise(p + vec3(0.0, 0.0, 0.0)),
    TDSimplexNoise(p + vec3(5.2, 1.3, 0.0)),
    TDSimplexNoise(p + vec3(0.0, 4.7, 6.1))
);
float n = TDSimplexNoise(p + warp * 2.0);

각 축에 다른 offset을 줘 세 개의 독립 noise를 만든 뒤, 그 벡터를 원래 좌표에 더해 다시 noise 호출. 같은 noise 함수를 4번 호출했을 뿐인데, 결과는 fluid-like 비대칭 패턴이 된다. Inigo Quilez의 "Domain Warping" 글이 표준 레퍼런스. TD에서 "curl noise처럼 보이는" 데모의 상당수가 실제로는 domain warping이다.

Noise를 normal에 적용하면 무엇이 다른가

P 대신 N에 noise를 더하면 mesh의 모양은 그대로지만 lighting이 noise를 따라 변한다 — bump mapping의 GPU 측 vertex 버전.

void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;
    vec3 n = TDIn_N();
    vec3 p = TDIn_P();
    vec3 perturb = vec3(
        TDSimplexNoise(p * 3.0),
        TDSimplexNoise(p * 3.0 + 17.0),
        TDSimplexNoise(p * 3.0 + 41.0)
    );
    N[id] = normalize(n + perturb * 0.3);
}

표면이 평평하더라도 lighting 계산이 normal을 보고 빛을 반사하므로 우둘투둘한 외관이 나온다. silhouette은 매끄럽다 — bump mapping의 한계이자 특성.

POP에서 같은 효과를 fragment shader에서 하는 것보다 vertex / point 측에서 하면 모든 픽셀이 vertex normal을 보간만 하므로 비용이 픽셀 수가 아닌 vertex 수에 비례한다. 단, 디테일은 vertex 밀도에 묶인다.

Animated noise — 시간 차원 추가

정적 noise는 한 frame의 패턴이다. 시간이 흐르는 noise를 만드는 패턴:

  1. 4D noise: 입력에 시간을 한 차원 더 추가. TDSimplexNoise(vec4(p, t)) — 차원이 늘면 비용이 증가하지만, simplex noise는 차원 증가에 따른 비용 증가가 (n+1) 정도로 완만하다. wiki 미확정 — TD의 TDSimplexNoise가 vec4 오버로드를 가지는지는 실험 필요.

  2. 3D noise + 시간 offset: TDSimplexNoise(p + vec3(0.0, 0.0, t * speed)). 가장 흔한 패턴. noise field가 z축으로 "흐르는" 느낌. fluid-like.

  3. 두 noise의 보간: 같은 좌표에서 시간 t에 따라 두 noise field 사이를 mix. 더 부드러운 변화.

  4. 시간을 frequency에 곱하기: TDSimplexNoise(p * (1 + sin(t))). frequency가 시간에 따라 변하므로 자라거나 줄어드는 느낌.

본 챕터의 curl noise에서는 (2) 패턴(p에 시간 offset 더하기)을 사용했다.

fBm — 옥타브 누적

자연스러운 외형은 한 octave가 아니라 여러 frequency의 noise를 더한 **fBm (fractional Brownian motion)**에서 나온다:

float fbm(vec3 p) {
    float total = 0.0;
    float amp = 0.5;
    float freq = 1.0;
    for (int i = 0; i < 5; i++) {
        total += amp * TDSimplexNoise(p * freq);
        amp *= 0.5;
        freq *= 2.0;
    }
    return total;
}

각 옥타브가 이전 옥타브의 절반 amplitude, 두 배 frequency. 결과는 lacunarity 2.0, gain 0.5의 표준 fBm. 5 octave면 noise 5번 호출. point가 100,000개라면 frame당 500,000번의 noise 호출이지만 GPU에서는 한 ms 정도면 끝난다.

Analytical derivative vs 유한 차분

curl noise를 만들 때 우리는 noise의 partial derivative가 필요하다. 위에서는 ±eps의 유한 차분(central difference)을 썼다 — 6번의 noise 호출.

더 빠른 방법은 noise 함수 자체가 derivative를 같이 반환하도록 만드는 것이다. Inigo Quilez의 "Gradient noise derivatives" 글이 이를 정확히 다룬다. value noise나 gradient noise는 lattice 기반이라 derivative를 닫힌 식으로 유도할 수 있다 — 함수 한 번 호출에 (value, dValue/dx, dValue/dy, dValue/dz) 네 값을 반환하는 형태.

vec4 noiseD(vec3 p);  // .x = value, .yzw = gradient

TD의 TDSimplexNoise()는 derivative 반환 변형이 wiki에 명시되지 않는다 — wiki 미확정 — 실험 필요. 만약 derivative 반환 함수가 없다면 curl/normal 계산에는 유한 차분이 유일한 길이다. 다만 작성자가 본인의 simplex noise 구현을 셰이더에 직접 넣으면 derivative 버전을 쓸 수 있다.

Quality vs Performance 모드의 사실상의 의미

simplex noise의 alternative 구현은 보통 두 갈래로 나뉜다:

performancequality가 정확히 어느 갈래를 선택하는지는 wiki가 명시하지 않는다. 실험적으로 확인할 사항:

본 챕터의 코드는 quality 모드를 기본으로 둔다.

Noise를 vertex displacement에 쓸 때 발생하는 normal 문제

P[id] = TDIn_P() + N * noise(P)로 displacement를 하면 N attribute는 그대로다. 즉 lighting 계산은 원래 sphere의 normal로 한다. 결과: 빛은 displacement 이전 곡면의 음영을, 형태는 displacement 이후의 모양을 따른다 — 시각적으로 어색하다.

올바른 해법: displacement 이후의 새 normal을 다시 계산. 두 가지 길.

  1. vertex shader stage에서 vertex의 tangent / bitangent로 displaced normal 재계산 — GLSL POP은 vertex shader가 아니므로 이 경로는 본 챕터의 범위 밖.
  2. 다음 POP으로 Normal POP을 다시 통과 — POP inventory의 Normal POP은 "Creates normal vectors and tangent vectors from incoming triangles or quads". GLSL POP 출력을 Normal POP에 다시 통과시키면 새 N이 계산된다.
Sphere POP → GLSL POP (P displacement) → Normal POP (N 재계산) → Geometry COMP

Normal POP의 비용은 triangle 1개당 한 cross product 정도. 100K triangle에서도 1 ms 미만.

  1. (보너스) noise의 analytical derivative가 있으면 새 normal을 displacement 안에서 직접 계산 가능: N_new = normalize(N_old - dot(N_old, grad) * grad) 형태. 식 자체는 Inigo Quilez의 글 참고.

Noise의 주기성과 tileable noise

Procedural texture에서 흔히 묻는 질문: "이 noise가 한 영역을 벗어나면 어떻게 되는가?" simplex noise는 무한 영역에 정의된 함수이고, 자연스러운 주기성이 없다 — 큰 값(예: p = (1e6, 1e6, 1e6))에서는 float 정밀도가 떨어져 격자 artifact가 보이기도 한다.

tileable noise가 필요할 때(텍스처 wrapping, 무한 terrain) 두 갈래 접근:

  1. Domain wrap: p_wrap = mod(p, period) 후 noise 호출 — 경계에서 불연속.
  2. Linear blend at boundary: 두 noise 호출의 가중 평균 — 매끄러움 보장. Inigo Quilez의 voronoise 패턴과 유사.

POP의 입자 displacement에는 tileable 요구가 적으므로 본 챕터에서는 다루지 않는다.

Noise의 좌표계 — local vs world

GLSL POP의 입력 P는 보통 object space (= local space)에 있다. Transform POP을 거치면 world space로 옮겨진다. noise를 적용할 때 어느 좌표에 noise를 호출하느냐가 결과를 크게 바꾼다:

두 의도는 다르다. 본 챕터의 sphere 예시는 sphere를 움직이지 않으므로 둘이 같지만, 입자나 mesh를 이동시킬 때는 명확히 선택해야 한다.

Texture 기반 noise

TDSimplexNoise()는 셰이더 안에서 함수로 호출한다. 다른 접근으로 Noise TOP의 출력을 GLSL POP의 sampler로 binding 해 텍스처 sample로 noise를 가져올 수도 있다.

Noise TOP1 (resolution 256x256, type: Simplex)
  → GLSL POP1의 Samplers 페이지: sampler0name=uNoiseTex, sampler0top=Noise TOP1

셰이더 안:

uniform sampler2D uNoiseTex;
float n = texture(uNoiseTex, P.xy * 0.5 + 0.5).r;

trade-off:

3D noise에는 3D 텍스처가 필요하고, sampler3D를 TD가 어떻게 노출하는지는 wiki 미확정 — 실험 필요.

Curl noise의 시각적 정체성

같은 입자 시뮬레이션에서 외력만 바꾸면 시각적 인상이 크게 달라진다.

curl noise가 fluid simulator의 대체재로 쓰이는 이유는 정확히 이 밀도 보존이다. Navier-Stokes를 실제로 풀지 않고도 incompressible flow의 시각적 인상을 얻는다. 비용은 noise 6–9번 호출, Navier-Stokes는 multi-pass + sparse linear solve. 시각적 정확도는 떨어지지만 실시간에서 입자 수만 개 단위까지 가능.

Domain warping의 깊이

domain warping을 한 단계만 적용하면 noise field가 비대칭으로 비틀어진다. 두 단계를 stack하면 더 복잡한 패턴.

vec3 warp1 = vec3(TDSimplexNoise(p), TDSimplexNoise(p + 17.0), TDSimplexNoise(p + 41.0));
vec3 warp2 = vec3(
    TDSimplexNoise(p + warp1 * 2.0),
    TDSimplexNoise(p + warp1 * 2.0 + 7.0),
    TDSimplexNoise(p + warp1 * 2.0 + 13.0)
);
float n = TDSimplexNoise(p + warp2 * 2.0);

각 단계에서 시각적 복잡성이 한 단계 더 증가한다. 한 단계가 fluid처럼 흐르는 noise, 두 단계가 marble처럼 휘는 noise, 세 단계가 더 복잡한 구름 / 동물 무늬에 가까운 결과. noise 호출 수가 (1+3) → (1+3+3) → (1+3+3+3)로 늘어나지만 GPU에서는 모두 1 ms 미만.

손작업 (Hands-on)

시작 파일

- Sphere POP1 (Primitive Type: Geodesic, Frequency: 5)
- Noise POP1                ← 노드 버전
- Null POP_node
- Text DAT_glsl             ← GLSL 코드
- GLSL POP1                 ← GLSL 버전
- Null POP_glsl
- Switch POP1               ← 두 버전 토글
- Geometry COMP1
- Render TOP1

연결:

Sphere POP1 ─┬─ Noise POP1   → Null POP_node ┐
             └─ GLSL POP1    → Null POP_glsl ┴ Switch POP1 → Geometry COMP1 → Render TOP1

Step 1 — Noise POP (노드 버전)

Noise POP1 파라미터:

Render에서 sphere가 noise에 의해 부풀고 일그러진다. 이 결과를 기준선으로 둔다.

Step 2 — GLSL POP (GLSL 버전, normal-방향 displacement)

GLSL POP1 파라미터:

Text DAT_glsl:

// Sphere의 모든 point를 원점 방향 normal로 noise만큼 변위.
// Step 1의 Noise POP 결과와 시각적으로 비교한다.
void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;

    vec3 p = TDIn_P();
    float n = TDSimplexNoise(p * 1.5);
    p += normalize(p) * n * 0.3;
    P[id] = p;
}

이 코드가 GPU에서 실제로 하는 일: 각 thread는 sphere 위의 한 point P를 읽는다. 그 point의 3D 좌표를 simplex noise 함수에 넣어 scalar n을 얻는다. 같은 point를 원점 방향(normalize(p))으로 n × 0.3 만큼 밀어낸다. sphere가 noise의 등치면(level set)을 따라 부푼다.

Switch POP1로 노드 버전과 GLSL 버전을 번갈아 비교. 동일한 frequency/amplitude를 정렬하면 거의 같은 외형이 나와야 한다 (단, Noise POP의 noise type이 simplex여야 하고 좌표계 origin이 같아야 함).

Step 3 — Curl noise로 fluid-like flow

Text DAT_glsl을 다음으로 교체:

// curl noise: divergence-free 벡터장. fluid처럼 점들이 흐른다.
// 본 셀은 한 프레임의 displacement만 적용 — 누적은 Ch.10/Ch.11에서.
uniform float uTime;

vec3 curlNoise3(vec3 p) {
    const float eps = 0.01;
    float n_yp = TDSimplexNoise(p + vec3(0.0, eps, 0.0));
    float n_yn = TDSimplexNoise(p - vec3(0.0, eps, 0.0));
    float n_zp = TDSimplexNoise(p + vec3(0.0, 0.0, eps));
    float n_zn = TDSimplexNoise(p - vec3(0.0, 0.0, eps));
    float n_xp = TDSimplexNoise(p + vec3(eps, 0.0, 0.0));
    float n_xn = TDSimplexNoise(p - vec3(eps, 0.0, 0.0));
    float dndx = (n_xp - n_xn) / (2.0 * eps);
    float dndy = (n_yp - n_yn) / (2.0 * eps);
    float dndz = (n_zp - n_zn) / (2.0 * eps);
    // 단순화된 3D curl: 세 편미분의 회전 조합
    return vec3(dndy - dndz, dndz - dndx, dndx - dndy);
}

void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;

    vec3 p = TDIn_P();
    vec3 v = curlNoise3(p * 1.2 + vec3(0.0, uTime * 0.1, 0.0));
    p += v * 0.05;
    P[id] = p;
}

GLSL POP1 Vectors 페이지에 uTime (float, expression: absTime.seconds) 추가.

이 코드가 GPU에서 실제로 하는 일: 각 thread가 simplex noise를 6번 호출한다 (각 축의 ±eps). 차분으로 편미분을 근사하고, 그 편미분을 적절히 조합해 curl 벡터를 얻는다. point를 그 벡터 방향으로 미세하게 (×0.05) 변위. 이 displacement는 매 프레임 입력 P에서 새로 계산되므로 누적되지 않는다 — Ch.10/Ch.11에서 누적 패턴을 다룬다.

Step 4 — performance vs quality 토글

GLSL POP1TDSimplexNoise() 파라미터를 performance ↔︎ quality로 바꿔 결과를 비교. quality 모드에서 grid artifact가 줄어드는지, performance 모드에서 frame rate가 개선되는지 확인. 정확한 ALU 비용 차이는 wiki 미확정 — 실험 필요.

노드 ↔︎ GLSL 매핑

같은 simplex noise displacement를 두 방식으로 비교.

노드 버전

Sphere POP1 → Noise POP1 (Attribute: P, Type: Simplex, Frequency: 1.5, Amplitude: 0.3)
            → Geometry COMP1 → Render TOP1

GLSL 버전

void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;
    vec3 p = TDIn_P();
    float n = TDSimplexNoise(p * 1.5);
    p += normalize(p) * n * 0.3;
    P[id] = p;
}
측면 Noise POP GLSL POP
표현력 정해진 noise type, 정해진 변위 방식 임의 noise 조합, fBm, curl, domain warp
입력 attribute 한 번에 하나 임의 개수의 attribute 동시 참조
디버깅 viewer에서 즉시 확인 shader 컴파일 에러 의존
비용 알 수 없음 (내부 최적화) 셰이더 인스펙션 가능

여기서 중요한 것: Noise POP은 "noise field를 attribute에 적용한다"는 의도를 명확히 표현하고, GLSL POP은 그 적용의 방식을 자유롭게 한다. 단순 displacement는 노드가 빠르고, curl/fBm/domain warp가 들어가는 순간 GLSL이 압승한다.

Noise displacement의 미적 조정 — 어떤 파라미터가 무엇을 바꾸나

본 챕터의 셰이더 p += normalize(p) * TDSimplexNoise(p * F) * A에서 F와 A의 역할을 분리해 이해해야 한다.

두 변수의 곱이 일정해도 외형이 같지 않다. F를 두 배로 하고 A를 절반으로 하면 패턴이 잘아지면서 변위 크기는 같다. 실용 권장:

fBm의 octave 수는 디테일 수준을 결정한다. 1 octave는 매끄럽고 단조롭다. 5 octave는 비싸지만 자연 지형 같은 self-similarity.

Noise displacement vs Surface noise (texture)

같은 noise가 두 다른 작업에 쓰인다.

사용 셰이더 stage 결과
Vertex displacement compute (GLSL POP) mesh의 모양 자체를 바꿈
Surface noise (color) fragment 모양은 그대로, 표면의 색만 noise 패턴
Bump mapping fragment 모양은 그대로, normal만 noise gradient
Parallax mapping fragment 모양은 그대로, fragment 위치를 가짜로 옮김

비용 측면: vertex displacement는 vertex 개수에 비례 (보통 ≤ 100K), fragment surface는 픽셀 개수에 비례 (200만). vertex displacement는 작업 후 mesh가 실제로 바뀌므로 그림자 / outline / collision에 자동 반영. fragment surface는 mesh의 모양은 그대로이므로 silhouette에 영향 없음.

CG에서 흔한 실수: 큰 displacement를 fragment bump로 시뮬레이션하면 silhouette은 sphere 그대로이고 표면만 우둘투둘. 의도와 다르다. silhouette을 바꿔야 할 displacement는 반드시 vertex stage(또는 compute로 P 갱신)에서 한다.

Noise를 일관성 있게 만드는 방법 — seed와 hash

같은 좌표에서 같은 noise 값이 나와야 한다 (deterministic). 그런데 한 scene에 여러 noise field를 두고 싶을 때, 각 noise가 서로 독립이어야 시각적으로 자연스럽다.

seed 패턴:

float n1 = TDSimplexNoise(p);
float n2 = TDSimplexNoise(p + vec3(31.4, 27.1, 13.7));
float n3 = TDSimplexNoise(p + vec3(-19.2, 53.8, 8.5));

큰 offset이 두 noise 호출을 사실상 독립으로 만든다. simplex noise는 자기상관(autocorrelation)이 좌표 거리 ~1 정도에서 0에 가까워지므로 offset 10 정도면 충분.

hash 함수로 좌표를 비틀어 seed를 만드는 방법도 있다 (Inigo Quilez의 hash 함수 시리즈). 본 챕터에서는 큰 상수 offset으로 충분.

확인 질문 (Self-check)

  1. 같은 simplex noise가 fragment shader에서는 vec2 uv를, GLSL POP에서는 vec3 P를 받는다. 두 사용에서 noise 함수 내부의 알고리즘은 어떻게 다른가?
  2. curl noise가 divergence-free라는 것은 점 밀도가 보존된다는 뜻이다. 만약 단순한 3-noise vector field (n(p+a), n(p+b), n(p+c))로 점을 움직이면 어떤 시각적 결과가 생기는가?
  3. fBm 5 octave는 frame당 noise를 몇 번 호출하는가? point가 1,000,000개면 총 호출 수는? GPU에서 이 비용이 왜 문제가 되지 않는가?
  4. TDSimplexNoise()performance로 두는 것의 trade-off는 무엇인가? 어떤 작업에 quality가 필요한가?
  5. normal-방향 displacement에서 self-intersection이 발생할 수 있다. 어떤 noise 파라미터(amplitude vs frequency)를 줄여야 그것을 막을 수 있나? 그 이유는?

연결 고리

이 챕터의 한 줄 명제

noise 함수는 영역에 무관하다. fragment shader에서 픽셀을, compute POP에서 점을 움직이는 같은 식이다.