Appendix B Reference Build 2025.31550

GLSL 스니펫

“GLSL POP / Advanced POP에 그대로 붙여 실행 가능한 14개 코드 조각.”

기준 build: TouchDesigner 2025.31550 함수 시그니처 1차 소스: research/02_chapter_briefs/glsl_pop_deep.md

이 디렉토리는 핸드북 본문(Ch.08~13, 15, 17)에서 사용하는 GLSL POP / GLSL Advanced POP 컴퓨트 셰이더의 재사용 가능한 최소 단위 스니펫 모음이다. 각 파일은 Compute Shader DAT에 그대로 붙여넣으면 컴파일되도록 작성되었다. 단, 셰이더는 항상 POP 파라미터와 짝을 이루기 때문에, 파일 상단 주석에 명시된 파라미터 설정을 따르지 않으면 컴파일 또는 런타임 오류가 발생한다.


1. 사용 절차

  1. DAT 준비. Text DAT를 하나 만들고 .glsl 파일의 내용을 그대로 붙여넣는다. 파일명을 그대로 DAT 이름으로 써도 무방하다.
  2. POP 준비. 스니펫 상단 주석의 node: 필드가 GLSL POP이면 GLSL POP, GLSL Advanced POP이면 GLSL Advanced POP을 만든다.
  3. Compute Shader 파라미터 연결. POP의 Compute Shader (computedat) 파라미터에 위 DAT 경로를 드래그하거나 직접 입력한다.
  4. Attribute Class 설정 (GLSL POP만 해당). 스니펫의 attr class: 필드와 동일하게 point / vertex / primitive 중 하나로 맞춘다.
  5. Output Attributes 설정. 스니펫의 outputs: 필드에 나열된 어트리뷰트들을 POP의 Output Attributes (outputattrs) 파라미터(또는 Advanced POP의 Point/Prim/Vert Output Attributes)에 wildcard 형태로 추가한다. 한 칸에 하나씩.
  6. Uniforms 페이지 설정. 스니펫의 uniforms / pages: 필드에 따라 Vectors / Samplers / Matrices / Arrays / Constants / Temp Buffers / Colors 페이지 중 해당하는 곳에 동일한 이름의 항목을 추가한다. 이름은 GLSL 코드의 식별자와 정확히 일치해야 한다.
  7. Number of Threads 설정. 별도 명시가 없으면 Auto(GLSL POP) 또는 Per Input Point(Advanced POP)로 둔다.

2. 파라미터 설정 관례

이 라이브러리 전체에서 다음 명명 규칙을 따른다.

종류 이름 접두사 예시 페이지
스칼라/벡터 uniform u uAmp, uFreq, uDt, uGravity Vectors
행렬 uniform u uModel, uMVP Matrices
텍스처 sampler u+Tex uTex, uHeight Samplers
카메라 관련 uCamera uCameraPos, uMVP Vectors / Matrices

Output Attributes에는 와일드카드 매칭이 적용된다. 단일 어트리뷰트를 적는 게 가장 안전하다(예: P, vel, visible).


3. 보편 bounds-check 관용구

모든 컴퓨트 셰이더의 main() 첫 두 줄은 다음과 같다.

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

이 두 줄은 TouchDesigner가 workgroup 크기(NVIDIA: 32, AMD: 64)에 맞춰 스레드 수를 올림 처리하면서 발생하는 SSBO 범위 밖 쓰기를 막는다. wiki 원문: "The if condition prevents the shader to write outside of the bounds of the allocated SSBO, which can lead to unpredictible results and crashes." Manual dispatch 모드에서는 TDIndex()TDNumElements()가 정의되지 않으므로 이 관용구를 쓸 수 없다. 그 경우는 부록 범위 밖이다.


4. GLSL POP vs GLSL Advanced POP 함수 명명

동작 GLSL POP (단일 클래스) GLSL Advanced POP (모든 클래스)
선택 클래스 입력 읽기 TDIn_AttribName() n/a (항상 클래스 접두사 사용)
다른 클래스 입력 읽기 TDInPoint_AttribName() / TDInVert_ / TDInPrim_ 동일
출력 변수 AttribName[] oTDPoint_AttribName[] / oTDVert_ / oTDPrim_
요소 수 TDInputNumElements() (선택 클래스) TDInputNumPoints() / TDInputNumVerts() / TDInputNumPrims()

5. 파일 인덱스

파일 POP 어트리뷰트 클래스 핵심 동작
01_identity_p.glsl GLSL POP point P를 그대로 복사
02_identity_p_advanced.glsl GLSL Advanced POP point P를 그대로 복사
03_sin_displace.glsl GLSL POP point Y축 사인파 변위
04_simplex_displace.glsl GLSL POP point 심플렉스 노이즈 방사 변위
05_curl_noise_velocity.glsl GLSL POP point 두 노이즈 샘플 차분으로 의사 curl-noise 속도장
06_particle_step_euler.glsl GLSL POP point 중력/끌림/감쇠로 1프레임 Euler 적분
07_neighbor_cohesion.glsl GLSL POP point Nebr 배열 어트리뷰트로 이웃 평균 위치 추종
08_transform_p_n.glsl GLSL Advanced POP point uModel 행렬로 P, 역전치 행렬로 N 변환
09_texture_displace.glsl GLSL POP point sampler2D 높이맵으로 P.y 변위
10_advanced_extra_output.glsl GLSL Advanced POP point main 출력 + Extra Output "trail"에 P 복제
11_frustum_cull_flag.glsl GLSL POP point uMVP로 클립공간 내/외 판정해 visible 0/1 기록
12_distance_lod.glsl GLSL POP point uCameraPos까지의 거리로 lod 0/1/2 기록
13_atomic_count.glsl GLSL POP point atomicAdd로 카운터 어트리뷰트 누적 (Read-Write 필수)
14_per_primitive_normal.glsl GLSL Advanced POP primitive perprimbatch 모드 스케치 (wiki 미확정 부분 포함)

6. 자주 만나는 오류


7. wiki 미확정 사항 (스니펫 내 주석으로 표시됨)

각 사항은 해당 스니펫의 주석에 // wiki 미확정 — 실험 필요로 표시되어 있다.

스니펫 코드

01_identity_p.glsl

// snippet: identity_p
// node: GLSL POP
// attr class: point
// inputs: P (point)
// outputs: P
// uniforms / pages: 없음
// 동작: 입력 P를 그대로 출력 P에 복사하는 최소 식별자 셰이더.
// 사용처: Ch.08 (첫 GLSL POP), Ch.07 (dispatch 모델 점검)

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

    // TDIn_P()는 TDIn_P(0, TDIndex())의 단축형이다.
    // 입력 0의 P를 현재 스레드 인덱스에서 읽어 출력 P에 기록.
    P[id] = TDIn_P();
}

02_identity_p_advanced.glsl

// snippet: identity_p_advanced
// node: GLSL Advanced POP
// attr class: all (single dispatch)
// inputs: point P
// outputs: point P  (Point Output Attributes에 P 등록)
// uniforms / pages: 없음
// 동작: Advanced POP에서 point P를 식별 복사. 클래스 접두사가 들어간 함수/배열명 사용.
// 사용처: Ch.12 (GLSL Advanced POP 입문)

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

    // GLSL POP의 TDIn_P / P[] 대신 Advanced POP은 항상 클래스 접두사를 붙인다.
    // TDInPoint_P()는 TDInPoint_P(0, TDIndex())의 단축형이다.
    oTDPoint_P[id] = TDInPoint_P();
}

03_sin_displace.glsl

// snippet: sin_displace
// node: GLSL POP
// attr class: point
// inputs: P (point)
// outputs: P
// uniforms / pages:
//   Vectors: uAmp (float), uFreq (float), uTime (float)
// 동작: P.y에 sin(uFreq * P.x + uTime) * uAmp를 더한다. XZ는 그대로.
// 사용처: Ch.08 (첫 GLSL POP — 가장 단순한 변위)

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

    vec3 p = TDIn_P();

    // 사인파 변위. uTime은 Vectors 페이지에서 absTime.seconds 등을 바인딩해 주입.
    // wiki 미확정 — 자동 노출되는 시간 uniform이 있는지 불확실하므로 명시 선언으로 우회.
    float wave = sin(uFreq * p.x + uTime) * uAmp;
    p.y += wave;

    P[id] = p;
}

04_simplex_displace.glsl

// snippet: simplex_displace
// node: GLSL POP
// attr class: point
// inputs: P (point), N (point, 선택)
// outputs: P
// uniforms / pages:
//   Vectors: uAmp (float), uFreq (float), uTime (float)
//   GLSL 페이지: TDSimplexNoise() = quality (또는 performance)
// 동작: 위치 기반 심플렉스 노이즈를 평가해 N(또는 정규화된 P) 방향으로 방사 변위.
// 사용처: Ch.09 (노이즈), Ch.10 (파티클 초기 분포)

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

    vec3 p = TDIn_P();

    // wiki 미확정 — TDSimplexNoise의 오버로드 집합과 반환 타입은 wiki 본문에 명시되지 않았다.
    //                 vec3 입력에 float 반환을 가정하고, 일치하지 않으면 swizzle/캐스트로 조정.
    float n = TDSimplexNoise(p * uFreq + vec3(0.0, 0.0, uTime));

    // 방사 방향. 입력에 N이 있으면 N을, 없으면 normalize(P)를 사용.
    // 이 스니펫은 N이 어트리뷰트로 존재한다고 가정.
    vec3 dir = normalize(TDIn_N());

    p += dir * (n * uAmp);
    P[id] = p;
}

05_curl_noise_velocity.glsl

// snippet: curl_noise_velocity
// node: GLSL POP
// attr class: point
// inputs: P (point)
// outputs: vel  (vec3, Create Attributes에서 미리 선언하거나 입력에 존재)
// uniforms / pages:
//   Vectors: uFreq (float), uAmp (float), uTime (float), uEps (float)
//   GLSL 페이지: TDSimplexNoise() = quality
// 동작: 두 스칼라 노이즈 필드의 유한차분으로 의사 curl-noise 속도장을 만든다.
//        실제 3D curl-noise는 3개 노이즈 필드의 ∇× 가 필요하지만, 학습용으로 2D xy curl을 시연.
// 사용처: Ch.09 (노이즈), Ch.10 (파티클 — 비발산 속도장 도입부)

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

    vec3 p = TDIn_P();
    vec3 q = p * uFreq + vec3(0.0, 0.0, uTime);

    // wiki 미확정 — TDSimplexNoise(vec3) -> float 가정.
    // ψ = noise(q). 2D curl: v = (∂ψ/∂y, -∂ψ/∂x, 0).
    float n_yp = TDSimplexNoise(q + vec3(0.0,  uEps, 0.0));
    float n_yn = TDSimplexNoise(q - vec3(0.0,  uEps, 0.0));
    float n_xp = TDSimplexNoise(q + vec3(uEps, 0.0,  0.0));
    float n_xn = TDSimplexNoise(q - vec3(uEps, 0.0,  0.0));

    float dpsi_dy = (n_yp - n_yn) / (2.0 * uEps);
    float dpsi_dx = (n_xp - n_xn) / (2.0 * uEps);

    vec3 v = vec3(dpsi_dy, -dpsi_dx, 0.0) * uAmp;
    vel[id] = v;
}

06_particle_step_euler.glsl

// snippet: particle_step_euler
// node: GLSL POP
// attr class: point
// inputs: P (point), vel (point, vec3)
// outputs: P, vel
// uniforms / pages:
//   Vectors: uGravity (vec3), uAttract (vec3), uAttractStrength (float),
//            uDrag (float), uDt (float)
// 동작: 1프레임 Euler 적분.
//        a = uGravity + uAttractStrength * (uAttract - P)
//        vel += a * uDt; vel *= (1 - uDrag * uDt); P += vel * uDt.
// 사용처: Ch.10 (파티클 시뮬), Ch.11 (Feedback POP과 함께)

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

    vec3 p = TDIn_P();
    vec3 v = TDIn_vel();

    // 가속도. 끌림 항은 중심 uAttract를 향한 선형 스프링.
    vec3 toCenter = uAttract - p;
    vec3 a = uGravity + uAttractStrength * toCenter;

    // 명시적 Euler — 시간 간격이 크면 발산하므로 uDt는 작게 유지.
    v += a * uDt;

    // 점성 감쇠. dt 의존이라 프레임레이트와 약하게 연동.
    v *= max(0.0, 1.0 - uDrag * uDt);

    p += v * uDt;

    P[id]   = p;
    vel[id] = v;
}

07_neighbor_cohesion.glsl

// snippet: neighbor_cohesion
// node: GLSL POP
// attr class: point
// inputs: P (point), Nebr (point, int 배열 — Neighbor POP 출력)
// outputs: P
// uniforms / pages:
//   Vectors: uBlend (float)   // 0..1, centroid 쪽으로 섞는 비율
//   GLSL 페이지: Output Attributes에 P 등록. (Nebr은 입력 그대로 사용.)
// 동작: Neighbor POP이 만든 Nebr[] 배열을 따라가 이웃 평균 위치(centroid)를 구한 뒤
//        P를 centroid 방향으로 uBlend만큼 끌어당긴다. boids의 cohesion 항.
// 사용처: Ch.13 (이웃 — Neighbor POP + GLSL POP)

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

    vec3 p = TDIn_P();

    // Nebr 배열의 크기는 컴파일 타임 상수로 노출된다.
    const uint K = cTDArraySize_Nebr;

    vec3 sum = vec3(0.0);
    uint count = 0u;

    for (uint k = 0u; k < K; ++k)
    {
        // TDIn_Nebr(inputIndex, elementId, arrayIndex)
        int nbId = TDIn_Nebr(0u, id, k);

        // Neighbor POP은 유효한 이웃이 없으면 음수를 채워 넣는다(관례).
        if (nbId < 0) continue;

        // 같은 클래스의 다른 인덱스 위치를 점프 읽기.
        sum += TDIn_P(0u, uint(nbId), 0u);
        count += 1u;
    }

    if (count > 0u)
    {
        vec3 centroid = sum / float(count);
        p = mix(p, centroid, clamp(uBlend, 0.0, 1.0));
    }

    P[id] = p;
}

08_transform_p_n.glsl

// snippet: transform_p_n
// node: GLSL Advanced POP
// attr class: all (single dispatch)
// inputs: point P, point N
// outputs: point P, point N  (Point Output Attributes에 P, N 등록)
// uniforms / pages:
//   Matrices: uModel        (mat4, 모델 변환)
//   Matrices: uNormalMatrix (mat3 또는 mat4, inverse-transpose(uModel)의 3x3 부분)
// 동작: 점 위치는 uModel로, 법선은 uNormalMatrix(=inverse-transpose의 상위 3x3)로 변환.
//        비균등 스케일에 대해 N 방향을 올바르게 유지하기 위한 표준 매핑.
// 사용처: Ch.06 (변환), Ch.12 (Advanced POP)

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

    vec3 p = TDInPoint_P();
    vec3 n = TDInPoint_N();

    // 위치는 동차좌표 1.0으로.
    vec4 pw = uModel * vec4(p, 1.0);
    oTDPoint_P[id] = pw.xyz / pw.w;

    // 법선은 inverse-transpose의 3x3 블록을 사용. mat4로 들어왔다고 가정하고 3x3 추출.
    // TouchDesigner Matrices 페이지에서 mat3로 직접 노출이 가능하면 mat3로 두는 편이 단순.
    mat3 Nm = mat3(uNormalMatrix);
    oTDPoint_N[id] = normalize(Nm * n);
}

09_texture_displace.glsl

// snippet: texture_displace
// node: GLSL POP
// attr class: point
// inputs: P (point), uv (point, vec2 — Texture Map POP 등으로 미리 생성)
// outputs: P
// uniforms / pages:
//   Samplers: uTex (sampler2D, Extend: hold/repeat 취향, Filter: linear)
//   Vectors:  uAmp (float)
// 동작: uv로 uTex를 샘플링해 R 채널을 높이로 사용. P.y에 uAmp * height 더하기.
// 사용처: Ch.04 (TOP→POP / 텍스처 변위), Ch.09 (노이즈를 텍스처로 굽기)

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

    vec3 p  = TDIn_P();
    vec2 uv = TDIn_uv();

    // wiki 미확정 — sampler 타입은 입력 TOP에 따라 결정. 본 스니펫은 2D TOP을 가정.
    float h = texture(uTex, uv).r;

    p.y += h * uAmp;
    P[id] = p;
}

10_advanced_extra_output.glsl

// snippet: advanced_extra_output
// node: GLSL Advanced POP
// attr class: point
// inputs: point P
// outputs:
//   Main: oTDPoint_P
//   Extra Output (Extra Outputs 페이지에 추가):
//     extraout0name = trail
//     extraout0pop  = (이전 프레임 trail의 소스가 되는 POP, 보통 Null/Trail POP)
//     extraout0ptattrs = P
// uniforms / pages:
//   Vectors: uShift (vec3)   // trail 분기에 적용할 변위
// 동작: main 출력에는 입력 P를 그대로 복사. 동시에 Extra Output "trail"의 P에는
//        uShift만큼 이동한 좌표를 기록. 다운스트림에서 GLSL Select POP으로 trail을 선택.
// 사용처: Ch.12 (Advanced POP의 Extra Outputs), Ch.11 (멀티-스트림 feedback)

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

    vec3 p = TDInPoint_P();

    // 메인 출력 — 단순 복사.
    oTDPoint_P[id] = p;

    // Extra Output "trail" — 동일 인덱스에 시프트된 위치 기록.
    // 함수/배열명은 extra output 이름을 접두사로 포함한다.
    // wiki 미확정 — extra output의 inputIndex 인자 유무는 wiki에 따르면 없음. 본 스니펫도 무인자.
    oTDPoint_trail_P[id] = p + uShift;
}

11_frustum_cull_flag.glsl

// snippet: frustum_cull_flag
// node: GLSL POP
// attr class: point
// inputs: P (point)
// outputs: visible  (int, Create Attributes에서 선언: type=int, components=1, default=1)
// uniforms / pages:
//   Matrices: uMVP (mat4)
// 동작: 각 점을 uMVP로 클립공간으로 보낸 뒤 |x|<=w, |y|<=w, 0<=z<=w 인지로 가시성 판정.
//        통과면 visible=1, 아니면 visible=0. 다운스트림에서 Delete POP 등으로 컬링.
// 사용처: Ch.14 (렌더), Ch.15 (인스턴싱 — 비표시 인스턴스 제거)

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

    vec3 p   = TDIn_P();
    vec4 clip = uMVP * vec4(p, 1.0);

    // 클립공간 절두체 테스트. OpenGL/Vulkan 모두 동일한 -w<=x,y<=w 규칙.
    // Z의 경우 TouchDesigner는 Vulkan 기반이라 0<=z<=w 범위를 사용한다고 가정.
    // wiki 미확정 — 정확한 NDC z 규약(OpenGL -1..1 vs Vulkan 0..1)은 환경 의존.
    bool inside =
        (abs(clip.x) <= clip.w) &&
        (abs(clip.y) <= clip.w) &&
        (clip.z >= 0.0) && (clip.z <= clip.w);

    visible[id] = inside ? 1 : 0;
}

12_distance_lod.glsl

// snippet: distance_lod
// node: GLSL POP
// attr class: point
// inputs: P (point)
// outputs: lod  (uint, Create Attributes에서 선언: type=uint, components=1, default=0)
// uniforms / pages:
//   Vectors: uCameraPos (vec3), uNear (float), uFar (float)
// 동작: 카메라까지의 유클리드 거리로 LOD 계층을 나눈다.
//        d < uNear: lod=0 (가장 세밀)
//        uNear <= d < uFar: lod=1
//        d >= uFar: lod=2 (가장 거침)
// 사용처: Ch.15, Ch.16 (인스턴싱 LOD), Ch.17 (시스템 매핑)

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

    vec3 p = TDIn_P();
    float d = distance(p, uCameraPos);

    uint k;
    if (d < uNear)      k = 0u;
    else if (d < uFar)  k = 1u;
    else                k = 2u;

    lod[id] = k;
}

13_atomic_count.glsl

// snippet: atomic_count
// node: GLSL POP
// attr class: point
// inputs: P (point)
// outputs: counter (uint, Create Attributes에서 선언: type=uint, components=1, default=0)
// uniforms / pages:
//   Vectors: uThreshold (float)
//   GLSL 페이지: Output Access = readwrite   (필수 — atomic 함수는 readwrite SSBO 필요)
//                Initialize Output Attributes = On  (counter[0]을 0으로 초기화)
// 동작: P.y > uThreshold 인 점의 수를 counter[0]에 atomicAdd로 누적.
//        리덕션의 가장 단순한 GPU 패턴. 결과는 첫 요소에만 의미 있음.
// 사용처: Ch.07 (dispatch + 동기화), Ch.13 (이웃 카운팅)

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

    vec3 p = TDIn_P();

    // 동일 SSBO에 모든 스레드가 RMW를 가하므로 readwrite + atomic이 필수.
    // wiki 원문: "Read-Write is necessary to perform atomic operations."
    if (p.y > uThreshold)
    {
        atomicAdd(counter[0], 1u);
    }

    // 주의: 이 셰이더는 counter[0]만 의미가 있다. counter[id] (id>0)은 정의되지 않은 값.
    //        Initialize Output Attributes가 켜져 있어야 모든 요소가 0으로 초기화된다.
    //        다운스트림에서는 첫 요소만 읽어 사용해야 한다.
}

14_per_primitive_normal.glsl

// snippet: per_primitive_normal
// node: GLSL Advanced POP
// attr class: primitive (writes), point (reads)
// inputs: point P, primitive vertex indices (via index buffer)
// outputs: oTDPrim_N (vec3)   (Prim Output Attributes에 N 등록, Create Attribs에서 class=primitive)
// uniforms / pages: 없음
// 파라미터:
//   Shader Dispatch Mode = perprimbatch
//   Number of Threads per Batch Mode = inputprim (또는 outputprim)
// 동작: 각 primitive batch마다 셰이더가 한 번 실행되고, 그 안에서 primitive 단위로 N을 계산.
//        삼각형의 첫 두 변으로 외적을 구해 facet normal을 만든다.
// 사용처: Ch.12 (Advanced POP), Ch.14 (렌더 — flat shading)
//
// wiki 미확정 — perprimbatch 모드에서 사용 가능한 빌트인의 정확한 시그니처는
//                Write_a_GLSL_POP의 "iterate over points of a primitive" 예제만 확정적이다.
//                아래 TDInputPrimVertsStartIndex() / TDInputNumVertsPerPrim() / TDInputPointIndex()는
//                같은 예제에서 사용된 형태를 그대로 따른다. perprimbatch와 single 모드 사이의
//                인덱스 의미(현재 처리 중인 prim 인덱스를 어떻게 얻는가) 차이는 실험 필요.

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

    // wiki 미확정 — 이 id가 primitive batch 안에서 "현재 primitive" 인덱스로 해석되는지,
    //                별도의 빌트인이 있는지는 wiki에 명시되지 않았다. 가장 안전한 가정은
    //                "Number of Threads per Batch Mode = inputprim일 때 id가 prim index"이다.
    const uint primId = id;

    // primitive의 verts 범위.
    uint vStart = TDInputPrimVertsStartIndex();
    uint vCount = TDInputNumVertsPerPrim();
    if (vCount < 3u)
    {
        oTDPrim_N[primId] = vec3(0.0, 1.0, 0.0);
        return;
    }

    // 첫 세 정점의 점 인덱스 → 위치.
    uint i0 = TDInputPointIndex(0u, vStart + 0u);
    uint i1 = TDInputPointIndex(0u, vStart + 1u);
    uint i2 = TDInputPointIndex(0u, vStart + 2u);

    vec3 p0 = TDInPoint_P(0u, i0, 0u);
    vec3 p1 = TDInPoint_P(0u, i1, 0u);
    vec3 p2 = TDInPoint_P(0u, i2, 0u);

    // facet normal — 정점 순서가 CCW일 때 외향.
    vec3 n = normalize(cross(p1 - p0, p2 - p0));
    oTDPrim_N[primId] = n;
}