08

Part III · Compute Core Chapter 08 of 18 Build 2025.31550

첫 GLSL POP

“P[id] = TDIn_P() 한 줄이 vertex buffer를 직접 쓰는 가장 작은 GPU 프로그램이다.”

이 챕터에서 정복할 CG 개념

왜 이게 중요한가

POP은 GPU 그래픽스 파이프라인의 데이터 모델을 노드 그래프로 노출시킨 학습 환경이다. 이 챕터에서 처음으로 그 데이터 모델을 노드가 아니라 코드로 직접 쓴다. 평소 fragment shader에서 gl_FragColor에 vec4를 대입하는 것은 framebuffer라는 SSBO 비슷한 것에 쓰는 일이고, 그 framebuffer는 다음 프레임의 sampler가 된다. GLSL POP에서 P[id] = ...에 vec3를 대입하는 것은 vertex buffer라는 SSBO에 쓰는 일이고, 그 buffer는 다음 노드의 입력, 혹은 Geometry COMP의 draw call 입력이 된다. 버퍼에 쓰는 가장 작은 프로그램을 손에 익히는 것이 본 챕터의 목표다. Ch.9 이후의 noise, particle, ping-pong, instancing, neighborhood — 모두 같은 Attr[id] = ... 한 줄을 변주한 것이다.

POP에서의 노출 지점

GLSL POP의 GLSL 페이지 파라미터 중 본 챕터에서 쓰는 것:

Vectors 페이지에서 uniform 추가:

GLSL 코드 측 핵심 시그니처 (wiki verbatim):

// 같은 class에서 읽기
attribType TDIn_AttribName(uint inputIndex, uint elementId, uint arrayIndex);
TDIn_AttribName() = TDIn_AttribName(0, TDIndex(), 0);

// 다른 class에서 읽기 (attribute class가 Point일 때 vertex/prim attribute 접근)
attribType TDInVert_AttribName(uint inputIndex, uint vertId, uint arrayIndex);
attribType TDInPrim_AttribName(uint inputIndex, uint primId, uint arrayIndex);

// 쓰기 — function이 아니라 array variable
attribType AttribName[];

이론

GLSL POP과 다른 GLSL POP의 차이

POP inventory에 따르면 GLSL 패밀리는 다섯 개다:

본 챕터는 가장 단순한 GLSL POP만 다룬다. 단일 class, 단일 출력, 단일 pass, 단일 dispatch — 즉 "한 attribute, 한 thread, 한 step"의 최소 단위. 이 단위에서 한 줄을 익히면 나머지 GLSL POP의 변주가 자연스럽게 따라온다.

Attribute = SSBO

TouchDesigner wiki의 단언: "Each attribute is a Shader Storage Buffer Object (SSBO)." 한 attribute(P, N, Cd, 사용자 정의 vel 등)는 GPU 메모리에 하나의 SSBO로 존재한다. point class의 P attribute이고 point가 1024개라면, P는 1024 × sizeof(vec3) = 12,288 byte의 buffer다. shader는 이 buffer를 vec3 P[]로 본다.

입력 P와 출력 P는 서로 다른 두 buffer다. TDIn_P()는 입력 buffer를 읽고, P[id] = ...는 출력 buffer에 쓴다. 이 분리 덕분에 모든 thread가 race 없이 동시 동작할 수 있다.

GLSL POP의 boilerplate — 작성자가 안 쓰는 부분이 있다

작성자가 Text DAT에 적는 코드는 void main() { ... } 한 함수다. 그러나 GPU가 실제로 실행하는 셰이더는 그것만이 아니다. GLSL POP은 컴파일 직전에 자동으로 boilerplate을 앞뒤로 붙인다.

boilerplate에 들어가는 것들:

작성자는 위 모든 것을 명시적으로 declare하지 않아도 된다. 셰이더의 헤더 영역은 GLSL POP의 파라미터 페이지가 책임진다. 이 추상화 덕분에 작성자는 SSBO binding 인덱스, layout qualifier, version directive 같은 boilerplate를 신경 쓰지 않고 알고리즘에만 집중할 수 있다.

trade-off: 어떤 보일러플레이트가 어떻게 생성되는지를 직접 볼 수 있는 toggle이 wiki에 명시되지 않는다 — wiki 미확정 — 실험 필요. textport에 컴파일 에러가 나면 그때 자동 생성된 코드의 일부가 라인 번호로 노출되기도 한다.

Output Attributes 파라미터의 의미

Output AttributesP를 적어 두지 않으면 셰이더 안에서 P[id] = ...는 컴파일되지 않는다. wiki 발췌: "For P[id] = TDIn_P(); to compile without error, P needs to be present in the input, and selected for writing in 'Output Attributes'."

Output Attributes는 와일드카드(*)도 받는다. 모든 attribute를 출력으로 노출하려면 *. 특정 attribute만 출력하려면 이름 나열.

수정하지 않은 attribute는 downstream POP으로 자동으로 reference 전달된다 (메모리 절약). wiki: "Unmodified input attributes for the POP from the first input are passed to downstream POP with references and don't use extra memory."

입력이 여러 개일 때 — 두 sphere를 한 셰이더에서 다루기

GLSL POP은 여러 입력을 받는다. 첫째 입력에서 P를 읽고 둘째 입력에서 다른 attribute를 가져와 합칠 수 있다.

void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;
    vec3 p0 = TDIn_P(0, id);    // 첫째 입력의 P
    vec3 p1 = TDIn_P(1, id);    // 둘째 입력의 P
    P[id] = mix(p0, p1, 0.5);   // 두 mesh를 blend
}

조건: 두 입력의 attribute class와 element 개수가 같아야 한다. 점 개수가 다르면 TDIn_P(1, id)의 id가 bound를 벗어날 수 있다 — 그러면 wiki가 보장하는 대로 마지막 element가 반환된다 (크래시는 아님).

이런 패턴은 morph target, blend shape, vertex animation의 기초다. point가 동일한 두 sphere를 가져와 morph 비율로 섞으면 vertex animation의 가장 단순한 형태가 된다.

Initialize Output Attributes — 왜 ON이 안전한가

이 토글이 ON이면 사용자의 compute shader가 호출되기 전에, GLSL POP은 별도의 사전 dispatch를 한 번 더 돌려 입력 attribute의 값을 출력 buffer에 그대로 복사한다.

OFF로 두면 두 가지 결과 중 하나다:

  1. 셰이더 코드가 모든 element를 빠짐없이 쓴다 → 정상.
  2. 셰이더 코드가 일부 element만 쓴다 → 나머지 element는 초기화되지 않은 GPU 메모리의 쓰레기 값을 가진다.

wiki 경고 verbatim: "Reading from unitialized values in downstream POPs can cause unpredictable behavior and crashes."

원칙: 모든 element를 매 frame 빠짐없이 쓰는 것이 확실한 셰이더에 한해서만 OFF로 끄고 한 번의 dispatch 비용을 절약한다. 의심스러우면 ON.

Output Access — writeonly vs readwrite

writeonly는 SSBO에 writeonly 한정자를 붙인다. 컴파일러는 같은 thread에서 같은 element를 read-after-write 못 하게 강제하고, 그 대신 더 공격적으로 최적화한다.

readwrite는 atomic 연산을 위해 필요하다. atomic은 같은 element를 여러 thread가 동시에 갱신할 때 race 없이 누적하려는 패턴 (histogram, particle compaction 등)이다. wiki 발췌: "Read-Write is necessary to perform atomic operations. In that case the output values should be initialized first, either using the toggle parameter, or during the first pass in a multi-pass POP (GLSL POP only)."

본 챕터는 한 thread가 한 element만 다루는 가장 단순한 1-pass 셰이더이므로 writeonly로 충분하다.

Attribute 종류별 layout과 component 수

GLSL POP에서 attribute는 type과 component 수로 정의된다. Create Attributes 페이지의 attr0type은 float / int 등 base type. attr0numcomps는 component 수 (1=scalar, 2=vec2, 3=vec3, 4=vec4).

자주 쓰이는 attribute의 표준 layout:

Attribute type comps 용도
P float 3 위치 vec3
N float 3 normal vec3
Cd float 3 RGB color (혹은 4 for RGBA)
Tex float 3 texture coord (보통 .xy 사용, z는 layer)
vel float 3 velocity vec3
age float 1 scalar
pscale float 1 scalar
Alpha float 1 scalar (혹은 Cd의 .a로 포함)
pid int 1 particle id

attr0isarray를 켜면 한 element가 array를 가진다. 예: bone weight 4개 = float, comps=1, isarray=on, arraysize=4. matrix attribute는 별도 matattr 블록.

다른 class의 attribute는 어떻게 읽나

GLSL POP은 한 노드에서 하나의 attribute class에만 쓴다. 그러나 읽기는 다른 class에서도 가능하다.

본 노드의 attrclass 같은 class 읽기 다른 class 읽기
point TDIn_AttribName() TDInVert_AttribName(), TDInPrim_AttribName()
vertex TDIn_AttribName() TDInPoint_AttribName(), TDInPrim_AttribName()
primitive TDIn_AttribName() TDInPoint_AttribName(), TDInVert_AttribName()

point에 쓰면서 vertex의 어떤 정보를 읽고 싶으면 TDInVert_AttribName()을 쓰면 된다. 단, 쓰기는 본 노드의 class에만 가능하다. 세 class에 동시에 쓰는 셰이더가 필요하면 GLSL Advanced POP을 써야 한다 (Ch.12).

Time uniform — 자동? 수동?

fragment shader에서 흔히 쓰는 iTime 같은 시간 입력이 GLSL POP에서 자동 노출되는지는 wiki 미확정 — 실험 필요. wiki는 GLSL POP의 자동 uniform 목록을 명시하지 않는다. 안전한 패턴은 Vectors 페이지에 직접 float 하나를 만들고 expression으로 absTime.seconds를 바인딩하는 것이다.

std140 / std430 layout과 alignment

POP attribute가 SSBO로 저장된다는 사실에는 한 가지 함의가 따라온다 — 메모리 정렬 규칙이다. GLSL의 SSBO는 보통 std430 layout을 따른다 (std140보다 packing이 더 친밀하다). 작성자는 한 attribute의 component 수를 정할 때 이 규칙을 의식해야 한다.

POP의 attribute는 보통 하나의 component 종류만(예: vec3 P) 가지므로 작성자가 직접 layout을 짜는 일은 거의 없다. 그러나 TDIn_P()가 vec3를 반환하고 그 vec3가 GPU 메모리에서 어떻게 자리잡는지를 알면 왜 vec3 attribute의 메모리가 vec4 attribute와 거의 같은지 같은 의문이 풀린다. LearnOpenGL의 "Advanced GLSL" 페이지 std140 절이 이 규칙의 표준 참고처다.

Uniform이 셰이더에 등장하는 방식

Vectors 페이지에 uTime이라는 float을 만들면, 셰이더 안에서 이 이름이 자동으로 노출된다. 작성자가 직접 uniform float uTime;을 적는 것은 명시적이지만, wiki에 따르면 boilerplate가 자동 declare하는 패턴이다.

실용 관찰:

Samplers 페이지에 추가한 TOP은 sampler2D 같은 형태로 노출되고, texture(samplerName, uv) 호출로 sampling. Arrays 페이지의 CHOP은 array 또는 texture buffer로 노출 (array0arraytype 선택). Matrices 페이지의 4x4 행렬은 mat4로 자동 declare.

Specialization constants (Constants 페이지)는 셰이더 컴파일 시점에 baked in 되는 상수다. 일반 uniform과 달리 매 dispatch마다 갱신되지 않으므로 inline 최적화가 가능하다. 본 챕터에서는 다루지 않지만 향후 성능 튜닝에서 유용.

입력 attribute가 reference로 전달된다는 사실

wiki 발췌: "Unmodified input attributes for the POP from the first input are passed to downstream POP with references and don't use extra memory."

본 GLSL POP이 P만 수정하고 N, Cd 같은 다른 attribute를 건드리지 않으면, downstream POP의 입력에는 N과 Cd의 새 buffer가 만들어지지 않고 입력 POP의 buffer가 그대로 참조된다. info panel에는 (r) 표시가 붙는다.

이 동작은 GPU 메모리 절약 측면에서 중요하다. attribute가 50개라도 GLSL POP에서 P만 수정하면 49개의 attribute는 새 메모리를 할당하지 않는다. 단, Output Attributes*로 모든 attribute를 등록하면 모두 allocation된다. 의도하지 않은 메모리 낭비를 막으려면 정말 수정하는 attribute만 명시한다.

TouchDesigner의 GLSL 버전과 그 의미

wiki 발췌: "TouchDesigner's main supported version of GLSL is 4.60." compute shader는 GLSL 4.30 이상에서 가능하다.

GLSL 4.60은 다음을 포함한다:

이 사실이 의미하는 것: GLSL POP 안에서 modern GLSL의 거의 모든 도구가 쓸 수 있다. imageStore, atomicAdd, barrier(), memoryBarrierShared() 등은 모두 가용. 단, GLSL POP은 shared memory 패턴을 wiki에서 별도로 문서화하지 않는다 — manual workgroup size에서는 가능할 것으로 추정되지만 wiki 미확정 — 실험 필요.

같은 셰이더의 두 dispatch가 결과가 다를 수 있는 경우

GLSL POP은 한 번의 dispatch만 보장한다. 같은 셰이더, 같은 입력, 같은 파라미터에서는 매번 같은 결과가 나온다 — 그것이 deterministic의 정의다. 그러나 다음 경우에는 결과가 다를 수 있다:

본 챕터의 단순 셰이더는 위 어디에도 해당하지 않으므로 완전한 deterministic이다.

입력 SSBO와 출력 SSBO의 분리 — race condition이 없는 이유

POP의 GLSL 모델에서 입력 P와 출력 P는 같은 이름을 가지지만 서로 다른 두 SSBO다. TDIn_P(0, j)는 입력 buffer의 j번 element를 읽고, P[id] = ...는 출력 buffer의 id번 element에 쓴다. 두 buffer가 분리되어 있기 때문에:

이 분리는 GPU 병렬화의 핵심 enabler다. CPU에서 같은 배열을 in-place로 수정하면 element 순서 의존성이 생기지만, GPU에서는 dispatch 전체가 한 atomic step처럼 동작한다. functional programming의 immutability와 같은 효과를 GPU 하드웨어로 강제한 것.

단, Output Access = readwrite이고 atomic 연산을 쓰면 출력 buffer가 mutate 가능해진다. 이때는 race condition을 신경 써야 한다.

TDInCache_ 패밀리 — Cache POP과의 짝

GLSL POP은 단일 frame의 입력만 보지 않는다. wiki verbatim signature:

attribType TDInCache_AttribName(uint inputIndex, uint cacheIndex, uint elemId, uint arrayIndex);

이 함수는 Cache POP의 N-frame history 중 cacheIndex번째 frame에서 attribute를 읽는다. trail / motion blur / temporal smoothing 같은 작업이 한 GLSL POP 안에서 가능해진다. point가 1024개고 cache가 60 frame이라면 GPU에는 1024 × 60 = 61,440개의 vec3가 보존되어 있고, 셰이더는 그 어느 element든 직접 읽을 수 있다.

Boundary handling — 함수가 자동으로 막아주는 것

wiki 발췌: "this performs bounds checking, return last elements if outside of bounds, last arrayIndex if arrayIndex is out of bounds."

TDIn_AttribName()은 elementId가 범위를 벗어나면 마지막 element를 반환한다. 즉 셰이더 안에서 elementId 계산이 잘못되어도 크래시는 나지 않는다. 단, 그 데이터가 의도와 맞는지는 작성자가 검증해야 한다. 이 보호막은 디버깅 비용을 크게 줄인다.

대비: P[id] = ...에서 id가 범위를 벗어나면 메모리 손상이 일어난다. 입력에는 안전망이 있고, 출력에는 없다. 그래서 출력 write 직전에 항상 bounds-check 한다.

셰이더 컴파일은 언제 일어나는가

GLSL POP의 Compute Shader DAT 내용이 변경되면, POP이 다음 cook 시 셰이더를 재컴파일한다. 컴파일은 driver가 GLSL 텍스트를 SPIR-V 중간 표현 후 GPU 네이티브 머신 코드로 번역하는 과정이다. 처음 cook은 수 ms ~ 수십 ms 걸릴 수 있고, 이후 cook은 0에 가까운 비용.

파라미터 페이지의 uniform 정의가 바뀌면 (Vectors에 새 항목 추가, type 변경 등) 셰이더 boilerplate가 바뀌므로 재컴파일이 일어난다. 입력 POP의 attribute 목록이 바뀌어도 마찬가지.

작성 중 자주 셰이더 텍스트를 수정하면 매번 컴파일이 일어난다. 이것이 일반적인 작업 흐름이고, 컴파일 비용은 작업을 방해할 만큼 크지는 않다.

textport에서 컴파일 에러 메시지를 본다. 에러 메시지의 줄 번호는 boilerplate가 앞에 붙은 결합된 셰이더의 줄 번호이므로, 사용자가 작성한 코드의 줄 번호와 어긋날 수 있다.

출력에만 등록한 새 attribute

Output Attributes에 입력에 없는 이름을 넣으면 어떻게 되나? 그것은 새 attribute이고, Create Attributes 페이지에서 type을 정의해야 한다.

Create Attributes:
  attr0name = density
  attr0type = float
  attr0numcomps = 1
  attr0value0 = 0.0  (default value)

이렇게 등록한 density는 셰이더 안에서:

새 attribute는 downstream POP에서 일반 attribute처럼 보인다. GPU 측에서는 새 SSBO 한 개가 할당된다. point 개수 × component 개수 × 4 byte. density 같은 float attribute는 1,000,000 point에서 4 MB.

성능 측면 — GLSL POP 한 번의 비용

본 챕터의 단순 셰이더 한 번의 비용을 가늠해 보자. 입자가 100,000개, NVIDIA RTX 3070 기준.

RTX 3070의 ALU throughput은 ~20 TFLOPS, memory bandwidth는 ~450 GB/s.

이론적 최소는 0.005 ms, 즉 5 µs. 실제로는 dispatch overhead (드라이버, 명령 buffer 생성), SSBO binding, 등 추가 비용으로 ~0.1–1 ms가 일반적이다. 100,000 입자를 60 fps로 갱신해도 GPU 시간의 1/16 ms = 6%만 소비.

이 분석이 의미하는 것: 단일 GLSL POP의 비용은 보통 무시할 수 있다. 병목은 같은 frame에 도는 다른 작업(render, multi-pass shading)이 거의 항상 더 크다.

손작업 (Hands-on)

시작 파일

- Sphere POP1 (Primitive Type: Geodesic, Frequency: 4)
- Text DAT1 (compute shader 코드)
- GLSL POP1
- Geometry COMP1
- Render TOP1

연결: Sphere POP1 → GLSL POP1 → Geometry COMP1 → Render TOP1

GLSL POP1 파라미터:

Step 1 — sine wave displacement

Text DAT1:

// Sphere를 y축으로 출렁이게 하는 가장 작은 GPU 프로그램.
// Vectors 페이지에서 uTime이라는 float uniform을 노출시켜야 한다.
uniform float uTime;

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

    vec3 p = TDIn_P();
    p.y += sin(p.x * 4.0 + uTime) * 0.2;
    P[id] = p;
}

이 코드가 GPU에서 실제로 하는 일: GPU는 sphere의 point 개수만큼 thread를 띄운다 (NVIDIA는 32 thread 단위로 올림). 각 thread는 자기 인덱스 TDIndex()의 P attribute를 입력 SSBO에서 vec3로 읽는다. 그 vec3의 y 성분에 시간과 x좌표에 따른 sin 값을 더한다. 결과 vec3를 같은 인덱스의 출력 P SSBO에 쓴다. 이 모든 일이 1024 (혹은 그 이상)개의 thread에서 동시에 일어난다. element 간 의존성이 없으므로 어떤 순서로 끝나든 결과가 같다.

Step 2 — TDTime 자동 노출 옵션

위 코드에서 uniform float uTime; 선언을 지우고, 셰이더 본문에서 uTime 자리에 TDTime.absTime 같은 표현을 시도해 본다.

void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;
    vec3 p = TDIn_P();
    // TDTime이 자동으로 노출되는지: wiki 미확정 — 실험 필요
    // p.y += sin(p.x * 4.0 + TDTime.absTime) * 0.2;
    P[id] = p;
}

본 핸드북에서는 Vectors 페이지로 직접 바인딩하는 Step 1의 방식이 권장 패턴이다. TDTime이 노출되더라도 명시적 uniform 바인딩이 의도와 의존성을 더 분명히 한다.

Step 3 — Initialize Output Attributes 끄기 실험

위 셰이더는 모든 thread가 매 frame P[id]에 무조건 쓴다. 따라서 Initialize Output Attributes를 OFF로 꺼도 정상 동작한다. OFF로 바꾸고 결과가 같은지 확인.

이제 셰이더를 다음과 같이 바꿔 일부 thread만 쓰게 만든다.

uniform float uTime;
void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;
    vec3 p = TDIn_P();
    if (mod(float(id), 2.0) < 0.5) {  // 짝수 id만 쓴다
        p.y += sin(p.x * 4.0 + uTime) * 0.2;
        P[id] = p;
    }
    // 홀수 id는 P[id]에 안 쓴다 → 미초기화 메모리
}

Initialize Output Attributes OFF → 홀수 id의 P가 GPU 메모리 쓰레기 값. 화면이 깨진다. Initialize Output Attributes ON → 홀수 id의 P는 입력값으로 자동 초기화. 짝수 id만 출렁인다.

관찰: ON이 왜 기본이고 왜 안전한지가 이 실험에서 가시화된다.

Step 4 — Output Attributes 비워두기

Output Attributes를 빈 칸으로 두면 P[id] = ...가 컴파일 에러를 낸다. 셰이더의 declare는 GLSL POP이 파라미터에서 자동 생성하기 때문이다.

노드 ↔︎ GLSL 매핑

같은 sine displacement를 노드만으로 구성한다.

노드 버전

Sphere POP1
  → Noise POP1 ??  ← Noise POP은 sin이 아니라 noise field.
                     순수 sin 변형은 Math POP의 Function 메뉴가 더 적합.
  → Math POP1 (Attribute: P, Function: ?)
  → Geometry COMP1
  → Render TOP1

순수 p.y += sin(p.x*4 + t)*0.2를 노드로 정확히 재현하려면 한 단계가 어렵다. POP inventory에서 Math POP은 "Takes one input attribute, performs math, writes result to a chosen attribute"이고, Math Combine POP은 "70+ operations"를 제공한다. 본 챕터 범위에서 노드 버전과 GLSL 버전이 완전히 동등한 결과를 내는 것보다 중요한 관찰은 다음이다:

측면 노드 chain GLSL POP
코드 줄 수 0 (그래프만) 3~5 줄
표현력 노드가 지원하는 함수에 한정 GLSL 전체 함수 (smoothstep, mix, mat3, ...)
임시 attribute 중간 단계마다 attribute 필요 로컬 변수 (vec3 p)
메모리 각 노드 출력이 새 SSBO 한 dispatch 안에서 끝
디버깅 각 단계를 viewer로 직접 봄 printf 없음. shader 컴파일 에러 의존

여기서 중요한 것: 노드 기반은 데이터 흐름의 각 단계가 SSBO로 물질화된다는 점을 가르치고, GLSL POP은 한 dispatch 안에서 임의의 산술이 가능하다는 점을 가르친다. 둘은 같은 GPU 모델의 두 면이다.

GLSL POP 작성의 표준 절차

본 챕터의 단순 displacement에서 출발해, 새 GLSL POP을 만들 때 거치는 순서를 정리한다.

  1. 어느 attribute class에 쓸지 정한다. point displacement면 point. mesh normal recalc이면 vertex. primitive 단위 작업(예: triangle 면적 계산)이면 primitive. 한 노드에서 한 class.

  2. 입력 POP의 attribute 목록을 확인한다. Info CHOP 또는 viewer로. 셰이더에서 읽으려는 attribute가 입력에 존재해야 한다. 없으면 Create Attributes에서 default와 함께 만든다.

  3. 출력 attribute를 결정하고 Output Attributes 파라미터에 적는다. 와일드카드 *는 모든 attribute를 출력으로 노출 (메모리 비용 ↑). 명시적 나열이 권장.

  4. Number of Threadsauto로 둔다. 특별한 이유가 없는 한 manual은 피한다.

  5. Compute Shader DAT을 만들고 bounds-check 관용구로 시작한다:

    void main() {
        const uint id = TDIndex();
        if (id >= TDNumElements()) return;
        // ...
        AttribName[id] = result;
    }
  6. Initialize Output Attributes는 ON으로 둔다. OFF로 끄는 결정은 모든 element를 매 frame 빠짐없이 쓴다는 확신이 있을 때만.

  7. uniform이 필요하면 Vectors / Colors / Samplers / Arrays / Matrices 페이지에 추가한다. 이름은 셰이더에서 그대로 uniform <type> <name>;으로 자동 노출된다.

  8. 컴파일 에러를 확인한다. GLSL POP의 info DAT 또는 textport. 에러 메시지는 셰이더 줄 번호 기준이지만, 자동 생성된 boilerplate(SSBO declaration 등)가 앞에 붙으므로 줄 번호가 어긋날 수 있다.

  9. 결과를 viewer에서 확인한다. POP viewer / Geometry COMP + Render TOP 양쪽. 예상과 다르면 셰이더 안에서 출력을 단순화해 isolate.

  10. 점차 복잡한 식을 추가한다. 한 번에 한 변화. 작동하는 코드에서 한 줄씩 추가해 문제 발생 지점을 분리.

디버깅 패턴

shader에는 printf가 없다. 디버깅은 시각화로 한다.

패턴 1 — id를 색으로 시각화

Output AttributesCd를 추가하고:

Cd[id] = vec3(float(id) / float(TDNumElements()), 0.0, 1.0 - float(id) / float(TDNumElements()));

id 분포를 그라데이션으로 본다. 첫 id와 마지막 id 사이가 매끄러우면 dispatch가 정상이다.

패턴 2 — 중간값을 색으로 export

displacement가 이상하게 보일 때, displacement 양 자체를 색으로:

float n = TDSimplexNoise(p * 1.5);
Cd[id] = vec3(n * 0.5 + 0.5);  // -1..1 → 0..1
P[id] = p + normalize(p) * n * 0.3;

displacement 크기가 noise 값을 시각적으로 검증.

패턴 3 — 한 attribute로 isolate

문제가 P인지 N인지 확실치 않으면 한 attribute만 출력에 두고 다른 것은 노드 chain 밖으로 빼낸다. 변수를 줄여 원인을 좁힘.

패턴 4 — Output Access를 readwrite로

같은 thread가 자기가 쓴 값을 다시 읽으려면 readwrite가 필요하다. 디버깅 중 임시로 readwrite로 두고 중간값을 재확인할 수 있다.

확인 질문 (Self-check)

  1. Output Attributes 파라미터에 attribute 이름을 적지 않으면 P[id] = ...가 컴파일되지 않는다. 왜인가? GLSL POP은 셰이더 텍스트에서 어떤 정보를 자동으로 추출해 사용하나?
  2. Initialize Output Attributes를 OFF로 두는 것이 안전한 정확한 조건은? 한 줄로 답하라.
  3. Output Accessreadwrite로 바꿔야 하는 시나리오 하나를 구체적으로 들어라 (atomic이 왜 필요한가).
  4. attribute class를 point로 둔 GLSL POP 안에서 vertex의 어떤 값을 읽고 싶을 때, 어떤 함수 이름을 호출하는가?
  5. 입력 P와 출력 P가 서로 다른 SSBO이기 때문에 가능한 일이 무엇인가? 만약 같은 buffer라면 어떤 race condition이 생기나?

연결 고리

이 챕터의 한 줄 명제

P[id] = TDIn_P(); 한 줄은 vertex buffer라는 SSBO에 직접 쓰는 가장 작은 GPU 프로그램이다. 이 한 줄을 변주한 것이 Ch.9 이후의 모든 셰이더다.