이 챕터에서 정복할 CG 개념
- per-element 시뮬레이션 상태(velocity, age, mass, lifetime 등)를 GPU에 보관하는 가장 자연스러운 형태가 custom attribute다.
- attribute의 class 선택(point / vertex / primitive)이 곧 메모리 레이아웃과 접근 패턴의 결정이다.
- attribute promote / demote / convert: 클래스 간 변환이 의미하는 것 — 데이터 단위가 바뀐다는 사실.
- vertex shader의
in변수, compute shader의 SSBO 행, Houdini의 point attribute가 모두 같은 개념의 다른 이름이다.
왜 이게 중요한가
GPU 프로그램의 첫 번째 설계 결정은 "이 데이터를 어디에 둘 것인가"다. velocity를 점에 두면 점 하나당 vec3 하나, 점이 100만 개면 12MB(float3 기준)다. velocity를 vertex에 두면 같은 점을 공유하는 면들마다 다른 속도를 줄 수 있지만 메모리는 N배가 된다. velocity를 primitive에 두면 면 하나당 하나 — 점 단위 시뮬레이션에는 무용지물이다. 이 결정을 한 번 잘못하면 그 위에 쌓는 모든 GLSL POP이 어색해진다.
같은 결정이 Houdini의 "Geometry attributes" 문서에서는 attribute class 표("point / vertex / primitive / detail")로 등장하고, Unity DOTS에서는 IComponentData가 어느 entity archetype에 붙느냐로 등장하고, WebGPU에서는 storage buffer의 binding 그룹 설계로 등장한다. POP은 이 결정을 노드 하나(Attribute POP) 위에 메뉴 하나로 노출시킨다. 그 메뉴를 만지는 동안 학습자는 GPU 데이터 모델의 첫 설계 결정을 손으로 해본다.
이 챕터에서 attribute는 영어 그대로, "어트리뷰트"라는 한국어 표기와 혼용하지 않는다. 다른 챕터에서는 한국어 표기를 쓸 수 있으나, 이 챕터 안에서는 영어 표기로 통일한다.
POP에서의 노출 지점
- Attribute POP: "Creates, renames, duplicates, deletes, or creates matrix attributes; you specify name, type, components, default." 새 attribute를 만드는 본진 노드. 이름, 타입, 컴포넌트 수, 기본값을 지정한다.
- Attribute Convert POP: "Converts an attribute from one class (point/vertex/primitive) to another." 클래스 변환 — point에 있던 attribute를 vertex로 옮기거나 그 반대. 옮기는 과정에서 데이터가 어떻게 재해석되는지가 학습 포인트.
- Random POP: "Generates random values for a new attribute or sets/adds/multiplies an existing attribute by random per-point values." 새 attribute의 초기값을 채우는 가장 빠른 방법.
- Math POP / Math Combine POP / Math Mix POP: attribute 간 산술.
P = P + vel * scalar같은 한 줄 갱신을 노드로. - Attribute Combine POP: 여러 POP 입력에서 attribute를 선택적으로 합친다. 여러 갈래의 시뮬레이션 상태를 다시 하나로 모을 때.
- Select POP: 입력의 일부 attribute만 통과시킨다. 디버깅에 유용.
이론
custom attribute의 정의
POP에서 attribute는 SSBO의 한 컬럼이다. 이름(예: vel), 타입(float / vec2 / vec3 / vec4 / int / uint / matrix 등), 컴포넌트 수, 그리고 어느 class에 속하는지로 정의된다. P(position), N(normal), Cd(color), Tex(texcoord), Pscale 등 일부는 표준 이름이지만, 그 외에는 모두 사용자가 만든다.
vel이라는 이름의 float3 attribute를 point class로 만들면 GPU에 다음 형태의 SSBO가 추가된다.
vel SSBO (point class, float3)
┌─────────────────────┐
│ vel[0] = (vx,vy,vz) │
│ vel[1] = (vx,vy,vz) │
│ vel[2] = (vx,vy,vz) │
│ ... │
└─────────────────────┘
length = point count
같은 이름을 vertex class로 만들면 SSBO의 길이가 vertex count로 바뀐다. primitive class면 primitive count.
per-element sim state의 의미
입자 시뮬레이션의 한 스텝은 보통 이렇게 표현된다.
v[i] += a[i] * dt
p[i] += v[i] * dt
i는 입자 인덱스, v는 velocity, a는 acceleration, p는 position. 이 식이 GPU 친화적인 이유는 i 사이에 의존성이 없다는 점이다. thread 하나가 인덱스 i 하나를 맡아 위 두 줄을 실행하면 끝난다. POP에서 v와 p를 point class attribute로 두면 SSBO 두 개가 점 수만큼의 길이로 GPU 메모리에 산다. compute shader는 thread당 한 점씩 위 식을 돌린다.
이것이 Ch.10의 입자 시뮬레이션의 전체 구조다. 이 챕터는 그 구조를 만들기 위한 첫 발걸음 — attribute 하나를 만들어 점에 붙이는 일 — 만 다룬다.
class 선택의 결과
| 선택 | 메모리 | 접근 패턴 | 적절한 경우 |
|---|---|---|---|
| Point | point 수만큼 | thread 1개 = 점 1개. 가장 자연스러운 dispatch. | 입자 위치/속도, 점 단위 시뮬레이션. |
| Vertex | vertex 수만큼 (= primitive count × verts-per-prim) | corner 단위 처리. 같은 점에서 면마다 다른 값. | per-corner texcoord, flat shading용 normal. |
| Primitive | primitive 수만큼 | 삼각형 단위. | face id, primitive group, face color. |
velocity를 vertex에 두는 결정은 거의 언제나 잘못된 결정이다. 같은 점이 여러 vertex slot에서 서로 다른 속도를 갖는 mesh는 시뮬레이션 다음 스텝에서 점 위치가 한 값으로 결정될 수 없다. velocity는 거의 항상 point class다.
반대로 정사면체에 "이 면은 트라이앵글 인덱스 몇 번?"같은 메타 정보를 attribute로 두고 싶다면 primitive class가 정답이다. 점에 두면 같은 점을 공유하는 두 면이 같은 값을 강제로 받는다.
attribute promote와 convert
Houdini의 Attribute Promote SOP에서 익숙한 동작이다. point에 있는 attribute를 primitive로 promote하면 각 primitive 내 점들의 값을 평균(또는 최댓값, 최솟값, 첫 번째)으로 reduce한다. 반대 방향(primitive → point)에서는 한 점에 인접한 모든 primitive의 값을 reduce한다.
POP의 Attribute Convert POP은 이 변환을 노드 하나로 제공한다. 위키 요약상 변환 방향과 reduce 방식의 정확한 메뉴 항목은 wiki 미확정 — 실험 필요. 본문에서는 "클래스 사이 변환은 reduce를 동반한다"는 사실만 짚는다.
데이터 단위가 바뀐다는 사실
이 챕터의 핵심은 다음 한 줄로 압축된다. attribute의 class 변환은 같은 데이터를 다른 단위로 재해석하는 작업이다. 한 점에 한 값이 붙어 있던 attribute를 vertex class로 promote하면 같은 값이 N개로 복제될 수도 있고, 평균을 통해 1개로 reduce될 수도 있다. 이 변환은 비용 없는 재해석이 아니다. GPU 메모리 안에서 실제로 새 SSBO가 할당되고 채워진다.
손작업 (Hands-on)
시작 파일
- Sphere POP1 (Geodesic, Frequency 3)
- Attribute POP1
- Random POP1
- Math POP1
- Render Simple TOP1
연결: Sphere POP1 → Attribute POP1 → Random POP1 → Math POP1 → Render Simple TOP1
단계 1 — vel이라는 점 attribute를 만든다
Attribute POP1을 선택한다. New Attribute 시퀀스 블록에서 다음을 설정한다.
- New Attribute Name:
vel - New Attribute Type: float3 (또는 vec3에 해당하는 메뉴)
- Class: Point (Attribute POP의 class 라벨은 wiki 미확정 — 실험 필요, point/vertex/primitive 중 point에 해당하는 항목)
- 기본값(Value 0/1/2): 0, 0, 0
Attribute POP1의 Info 패널에서 vel attribute가 point class로 추가된 것을 확인한다. 이 단계의 관찰 포인트는 단 하나 — point buffer에 새 컬럼이 하나 늘었다는 사실이다.
단계 2 — Random POP으로 vel을 채운다
Random POP1을 선택한다. 대상 attribute로 vel을 지정한다. 결과를 set 모드로 두고 범위를 -0.5에서 0.5 정도로 설정한다. Render Simple TOP1에서 보이는 구는 아직 그대로다. 점은 움직이지 않았다. velocity는 단지 점이 가진 새 attribute일 뿐 위치에 더해지지 않았기 때문이다.
단계 3 — Math POP으로 P에 vel을 더한다
Math POP1을 설정한다. 입력 attribute로 P와 vel을 사용하고, 결과를 P = P + vel * 0.1처럼 출력한다. 정확한 파라미터 라벨은 Math POP의 시퀀스 블록 구성을 따른다 — operation을 "add", 두 번째 피연산자에 vel, 스케일러 0.1. Render Simple TOP1에서 구가 약간 부풀어 보인다. 점마다 서로 다른 무작위 방향으로 0.1만큼 밀려났다.
이 시점에 학습자는 첫 attribute가 점에 붙는 순간을 체험했다. 시간이 흐르는 시뮬레이션은 아직 아니다(매 프레임 vel을 사용하지만 vel 자체가 갱신되지 않는다). 그 갱신은 Ch.11의 feedback POP에서 다룬다.
단계 4 — Attribute Convert POP으로 클래스를 옮긴다
Math POP1 다음에 Attribute Convert POP1을 끼우고 vel을 vertex class로 변환한다. Info 패널에서 vel이 vertex class로 옮겨졌고 SSBO 길이가 vertex count로 바뀐 것을 본다. 이 변환의 결과로 같은 점이 서로 다른 vertex slot에서 다른 vel을 가질 수 있게 됐지만, 시각적으로는 아무것도 바뀌지 않는다 — Math POP은 이미 P를 갱신한 뒤이고, 그 이후 vel은 더 이상 쓰이지 않기 때문이다. 메모리 모양만 바뀌었다. 이것이 attribute promote / convert의 비용을 직관적으로 보여주는 자리다.
노드 ↔︎ GLSL 매핑
위 손작업 전체(Attribute POP → Random POP → Math POP)를 GLSL POP 한 노드에서 한 번에 처리할 수 있다. Random POP의 "임의 vel 채우기" 부분은 GLSL에서 hash 함수로 대체한다.
// GLSL POP
// Attribute Class : point
// Number of Threads : Auto
// Output Attributes : P, vel
// Create Attributes : vel (float3, default 0)
//
// 한 컴퓨트 dispatch로 vel을 초기화하고 P에 더한다.
float hash11(uint n) {
n = (n ^ 61u) ^ (n >> 16u);
n *= 9u;
n = n ^ (n >> 4u);
n *= 0x27d4eb2du;
n = n ^ (n >> 15u);
return float(n & 0x00ffffffu) / float(0x01000000u);
}
vec3 hash33(uint n) {
return vec3(
hash11(n * 3u + 0u),
hash11(n * 3u + 1u),
hash11(n * 3u + 2u)
) * 2.0 - 1.0;
}
void main() {
const uint id = TDIndex();
if (id >= TDNumElements()) return;
vec3 p = TDIn_P();
vec3 v = hash33(id) * 0.5; // [-0.5, 0.5]^3 무작위
vec3 p_new = p + v * 0.1;
P[id] = p_new;
vel[id] = v;
}
이 코드가 GPU에서 실제로 무엇을 하고 있는가. 첫째, TDIndex()로 현재 thread의 1D 인덱스를 받는다. 둘째, 표준 bounds-check 관용구로 workgroup 라운드업 잉여 thread를 즉시 차단한다. 셋째, TDIn_P()로 입력 P attribute를 읽는다. 넷째, 인덱스 기반의 결정론적 hash로 -0.5에서 0.5 범위의 vec3를 만들어 vel로 쓴다. 다섯째, P에 vel × 0.1을 더한 결과를 P SSBO의 같은 슬롯에 쓴다. 여섯째, vel SSBO의 같은 슬롯에 v 자체도 저장한다. 결과적으로 한 dispatch 안에서 두 SSBO에 동시에 쓴다 — 노드로는 세 노드(Attribute POP, Random POP, Math POP)에 해당하는 작업이다.
같은 결과를 GLSL Advanced POP으로도 쓸 수 있다. 차이는 출력 SSBO의 이름 prefix다.
// GLSL Advanced POP
// Shader Dispatch Mode : single
// Number of Threads : Per Input Point
// Point Output Attributes : P, vel
// Create Attributes : vel (float3, class = point, default 0)
//
// 같은 결과를 Advanced POP으로. 출력 prefix가 oTDPoint_.
float hash11(uint n) {
n = (n ^ 61u) ^ (n >> 16u);
n *= 9u;
n = n ^ (n >> 4u);
n *= 0x27d4eb2du;
n = n ^ (n >> 15u);
return float(n & 0x00ffffffu) / float(0x01000000u);
}
vec3 hash33(uint n) {
return vec3(
hash11(n * 3u + 0u),
hash11(n * 3u + 1u),
hash11(n * 3u + 2u)
) * 2.0 - 1.0;
}
void main() {
const uint id = TDIndex();
if (id >= TDNumElements()) return;
vec3 p = TDInPoint_P(); // = TDInPoint_P(0, id, 0)
vec3 v = hash33(id) * 0.5;
vec3 p_new = p + v * 0.1;
oTDPoint_P[id] = p_new;
oTDPoint_vel[id] = v;
}
이 코드는 GLSL POP 버전과 같은 일을 한다. 차이는 두 가지다. 첫째, 입력 reader가 TDInPoint_P()로 class prefix를 명시적으로 갖는다 — Advanced POP은 한 셰이더 안에서 세 class 모두 읽고 쓸 수 있으므로 prefix가 필수다. 둘째, 출력 SSBO가 oTDPoint_P, oTDPoint_vel로 prefix를 갖는다. Advanced POP에서 vel 같은 새 attribute를 만들 때는 Create Attributes 페이지의 시퀀스 블록에서 attr0class를 point로 명시해야 한다 — GLSL POP은 attrclass 한 파라미터로 셰이더 전체의 class가 고정되지만, Advanced POP은 attribute별로 class를 따로 지정한다.
두 구현 모두 한 dispatch 안에서 두 SSBO에 쓴다는 점에 주목한다. 노드로는 세 노드를 거치는 흐름이 GPU 측에서는 thread당 몇 줄의 산술로 끝난다. 노드 그래프는 가독성과 단계별 검증을, GLSL POP은 한 dispatch의 응축을 제공한다.
확인 질문 (Self-check)
- velocity attribute를 vertex class로 두면 시뮬레이션이 왜 다음 스텝에서 어색해지는가?
- point class attribute를 vertex class로 convert할 때 SSBO의 길이는 어떻게 바뀌는가? 그 변화는 메모리에서 실제로 일어나는가, 아니면 단순한 재해석인가?
- Houdini의 Attribute Promote SOP가 reduce 방식(average / min / max / first)을 지정한다는 사실은, 클래스 변환의 정보 손실에 관해 무엇을 함의하는가?
- GLSL POP에서
vel을 새로 만들었지만 Output Attributes에 적지 않으면 어떤 일이 일어나는가? - 같은 attribute 이름(
vel)이 point class와 vertex class에 동시에 존재할 수 있는가? Advanced POP의 출력 SSBO 이름 규칙은 그 가능성에 어떻게 대응하는가?
연결 고리
- Houdini docs — "Geometry attributes" (https://www.sidefx.com/docs/houdini/model/attributes.html). attribute의 정신적 선조. point / vertex / primitive / detail의 네 class와 attribute precedence 규칙.
- Houdini docs — "Attribute Promote SOP" (https://www.sidefx.com/docs/houdini/nodes/sop/attribpromote.html). 클래스 간 변환과 reduce 방식의 정전.
- Houdini docs — "VEX" (https://www.sidefx.com/docs/houdini/vex/index.html). per-element attribute I/O의 Houdini판. GLSL POP과의 사고법 비교.
- Real-Time Rendering 4ed §3 "The Graphics Processing Unit", "Programmable Shader Stage" 섹션. attribute가 vertex shader의
in변수로 들어가는 자리. - TouchDesigner Wiki — "Attribute POP", "Attribute Convert POP" (https://docs.derivative.ca/Attribute_POP, https://docs.derivative.ca/Attribute_Convert_POP).
이 챕터의 한 줄 명제
데이터를 어느 attribute class에 둘 것인가는 GPU 프로그램의 첫 번째 설계 결정이며, Attribute POP의 메뉴 하나가 그 결정을 노드 그래프 위에 노출시킨다.