Graphics Programming

[Marco Scabia] 02 AGAL이란 본문

Season 1/플래시

[Marco Scabia] 02 AGAL이란

minseoklee 2011. 10. 13. 00:29

원문
http://www.adobe.com/devnet/flashplayer/articles/what-is-agal.html



이 글에서는 셰이딩 언어로 작업하는 것을 소개합니다. 저는 Stage3D API에 포함된, AGAL(Adobe Graphics Assembly Language)이라는 저수준 셰이딩 언어의 기본적인 사용법을 다룰 것입니다. 여러분은 AGAL이 무엇이고, 어떻게 작동하는지 보고 Stage3D 기반 액션스크립트 애플리케이션의 일부로서 사용하는 방법을 배울 것입니다.

셰이딩 언어 이해하기

AGAL을 파고들기 전에 셰이딩 언어가 무엇이고 셰이더를 제작하려면 이 언어를 어떻게 사용할 지를 먼저 짚고 넘어가는 게 좋겠습니다.

셰이더는 액션스크립트로 작성하지 않습니다. C++이나 여타 다목적 언어로도 작성하지 않습니다.

셰이더는 대개 셰이딩 언어라는 특별한 언어로 작성합니다.

셰이더는 GPU에서 실행되는 프로그램이므로 셰이더를 코딩하는 가장 효율적인 방법은 특별히 GPU를 위해 고안된 언어를 사용하는 것입니다. 이것이 여러분이 CPU를 위한 코딩용으로 설계된 범용 언어 대신 특수한 셰이딩 언어를 사용해서 셰이더를 작성해야 하는 이유입니다.

두 표준 네이티브 3D 플랫폼(OpenGL과 DirectX)에서 여러 해 동안 쓰인 셰이딩 언어들이 있습니다. GLSL과 HLSL이 그 중에서도 가장 널리 쓰이는 셰이딩 언어입니다.

Stage3D API를 위해 어도비는 GPU를 위한 프로그램을 제작하기 위한 새로운 셰이딩 언어 AGAL과 픽셀 벤더 3D를 만들었습니다.

AGAL과 픽셀 벤더 3D 훑어보기

AGAL(Adobe Graphics Assembly Language)은 어셈블리 언어이므로 GPU가 실제로 실행하는 것과 아주 밀접합니다. GPU(이 문제에 관해서는 CPU 또한)는 변수, 클래스 따위가 있는 액션스크립트 같은 고수준 언어를 직접 이해하지 못합니다. GPU는 초등적인 기계어 명령만을 이해합니다. 파이프라인의 어딘가에는 고수준 언어의 복잡한 명령을 더 간단하고 저수준인 일련의 기계어 명령으로 해석해주는 컴파일러가 있습니다.

AGAL을 써서 여러분은 GPU가 이해하는 명령과 비슷한 저수준 명령을 직접 작성할 수 있습니다.

픽셀 벤더 3D는 보다 고수준 언어이므로 AGAL보다 이해하기 쉽습니다. 픽셀 벤더 3D는 픽셀 벤더의 확장판이지만 3D 그리고 셰이더와 작동하도록 개선되었습니다.

픽셀 벤더 3D와 AGAL 중 하나를 고를 때면 각자 장단점이 있습니다. 픽셀 벤더 3D는 분명 더 쓰기 쉽고 그래서 복잡한 셰이더를 작성할 때 시간이 덜 걸립니다.

반면에 AGAL은 GPU가 일하는 방식과 밀접합니다. 결국 여러분은 렌더링 파이프라인에서 실제로 무슨 일이 벌어지는지 더 잘 이해하게 됩니다. 컴파일러에게 맞기는 대신 여러분이 직접 셰이더를 최적화할 수도 있습니다. 그러니 여러분이 AGAL에 시간을 투자하면 보다 최적화된 셰이더를 만들 수 있을 것입니다.

Stage3D가 작동하는 방식을 배우는 게 목표일 때도 AGAL은 좋은 선택입니다. AGAL로 여러분은 GPU에 보다 근접한 명령을 실행할 수 있습니다. 따라서 렌더링 파이프라인에서 벌어지는 일을 더 쉽게 이해할 수 있습니다.

또 하나 중요한 것이 있는데, 픽셀 벤더 3D를 쓸 때는 여러분이 컴파일 시간에 셰이더를 미리 컴파일해야 합니다. AGAL을 쓰면 셰이더가 셰이더 프로그램 문자열의 형태로 있게 되며 실행 시간에 목적 코드와 합쳐집니다. 그래서 AGAL을 쓰면 동적으로 셰이더를 제작할 수 있습니다.

저는 Stage3D로 작업하는 것을 배우겠다면 AGAL 학습으로 시작하는 게 최선의 선택이라고 생각합니다.

AGAL 문법 살펴보기
 
AGAL은 어셈블리어입니다. 액션스크립트 문법으로 코딩하는 데 익숙하시다면 AGAL 같은 어셈블리어는 처음 보면 조금 괴상할 지도 모르겠습니다.

이것이 AGAL 정점 셰이더의 예입니다.

m44 op, va0, vc0
mov v0, va1

여러분이 각 어셈블리 라인이 의미하는 바를 알아볼 수 있게 위 예시의 문법을 설명하겠습니다.

셰이더의 모든 줄이 명령으로서, opcode라는 세 자리 문자열로 명시됩니다.

AGAL 코드의 문법은 다음과 같이 구성됩니다.

<opcode> <destination>, <source 1>, <source 2 or sampler>
 
이것이 핵심이니 이 문법을 숙지하시면 AGAL이 알아볼 수 없는 횡설수설로 보이지는 않을 것입니다.

opcode 다음의 것들은 명령에 따라 다릅니다. 한 destination이 있을 것이고 source도 한 개나 두 개가 있을 것입니다. destination와 source는 레지스터라고 부릅니다. 레지스터는 GPU에 있는 작은 메모리 영역으로서 셰이더가 사용합니다. 뒤에서 레지스터를 보다 자세히 다룰 것입니다. source들은 명령에 사용한 값들을 담고 destination은 결과가 저장될 위치입니다.

AGAL의 주요 opcode들
 
AGAL에는 30여 개의 opcode가 있습니다. 모든 opcode의 목록은 레퍼런스의 Program3D 페이지에서 찾을 수 있습니다. 여기 가장 흔한 opcode가 몇 개 있습니다.

mov: source1에서 destination으로 데이터를 성분 별로 옮긴다.
add: 성분 별로 destination = source1 + source2
sub: 성분 별로 destination = source1 – source2
mul: 성분 별로 destination = source1 * source2
div: 성분 별로 destination = source1 / source2
dp3: source1과 source2간의 내적 (성분 세 개)
dp4: source1과 source2간의 내적 (성분 네 개)
m44: source1의 4 성분 벡터와 source2의 4x4 행렬간 곱셈
tex: 텍스쳐 샘플. 텍스쳐 source2의 좌표 source1에서 가져온다.
 
그림 1과 그림 2에서 모든 AGAL 명령어를 볼 수 있습니다.

그림 1. AGAL 메모리, 산술, 삼각법, 대수 관련 opcode들.

그림 2. AGAL 벡터, 행렬, 조건문, 텍스쳐 추출 관련 opcode들.

AGAL 레지스터로 작업하기

AGAL은 액션스크립트나 다른 고수준 언어와 달리 데이터를 저장하는 데 변수를 사용하지 않습니다. AGAL은 그저 레지스터를 씁니다.

레지스터는 GPU에 있는 작은 메모리 영역으로 셰이더가 실행 중에 사용할 수 있습니다. 레지스터는 AGAL 명령의 source와 destination을 저장하는 데 쓰입니다.

이들 레지스터를 통해 여러분이 셰이더에 매개변수를 넘길 수도 있습니다.

각 레지스터는 128 비트 크기인데 네 부동소수점 값을 포함한다는 것을 의미합니다. 각각의 값을 그 레지스터의 성분이라고 부릅니다.

좌표 접근자(xyzw)로도 색상 접근자(rgba)로도 레지스터 성분들에 접근할 수 있습니다.

레지스터의 첫 성분에는 이렇게 접근할 수 있습니다.

<레지스터이름>.x

이 코드도 마찬가지입니다.

<레지스터이름>.r

레지스터는 좌표 같은 데이터를 포함하기도 하고 색상 데이터를 포함하기도 합니다. 알맞은 접근자를 사용하여 여러분의 코드를 더욱 깔끔하고 읽기 쉽게 만들 수 있습니다.

위의 opcode들 중 일부, 이를테면 add 같은 것들은 성분 별로 명령을 수행합니다. 이는 덧셈 명령이 각각의 성분마다 수행되어 x 성분은 x 성분끼리, y 성분은 y 성분끼리 이렇게 더해진다는 뜻입니다.

쓸 수 있는 레지스터의 종류는 여섯 가지입니다.

1. 속성(attribute) 레지스터
이 레지스터들은 정점 셰이더의 입력인 VertexBuffer의 정점 속성(attribute) 데이터를 참조합니다. 그러므로 이 레지스터는 정점 셰이더에서만 사용할 수 있습니다.

이 레지스터가 정점 셰이더가 처리하는 주요 데이터 스트림입니다. VertexBuffer의 각 정점 속성은 고유의 속성 레지스터를 가집니다.

VertexBuffer 속성에 특정 속성 레지스터를 할당하려면 Context3D:setVertexBufferAt()을 알맞은 인덱스와 함께 사용하면 됩니다.

그러면 셰이더에서는 문법 va<n>을 따라 속성 레지스터에 접근합니다. <n>은 속성 레지스터의 인덱스 숫자입니다.

정점 셰이더에서 쓸 수 있는 속성 레지스터는 총 8개입니다.

2. 상수 레지스터
이 레지스터들은 액션스크립트에서 셰이더로 매개변수를 넘길 목적으로 쓰입니다. Context3D::setProgramConstants() 류의 함수를 씁니다.

셰이더에서는 정점 셰이더의 경우 vc<n>, 픽셀 셰이더의 경우 fc<n>이라는 문법을 가지고 이 레지스터들에 접근합니다. <n>은 상수 레지스터의 인덱스 숫자입니다.

정점 셰이더에서는 상수 레지스터를 128개 쓸 수 있습니다. 픽셀 셰이더에서는 28개를 쓸 수 있습니다.

3. 임시 레지스터
이 레지스터들은 셰이더에서 일시적인 계산을 위해 씁니다. AGAL이 변수를 쓰지 않기 때문에 여러분의 코드를 통틀어, 임시 레지스터를 사용하여 데이터를 저장하게 될 것입니다.

임시 레지스터에는 vt<n> (정점 셰이더)와 ft<n> (픽셀 셰이더)로 접근할 수 있고 <n>은 인덱스입니다.

정점 셰이더와 픽셀 셰이더 모두 사용 가능한 임시 레지스터는 8개입니다.

4. 출력 레지스터
출력 레지스터는 정점 셰이더와 픽셀 셰이더의 계산 결과를 저장하는 데 쓰입니다. 정점 셰이더의 경우 이 출력은 정점의 위치고 픽셀 셰이더의 경우 픽셀의 색깔입니다.

이 레지스터들에는 정점 셰이더의 경우 op, 픽셀 셰이더의 경우 oc로 접근할 수 있습니다.

정점 셰이더든 픽셀 셰이더든 출력 레지스터는 오로지 하나입니다.

5. 변환(varying) 레지스터
varying을 뭐라고 할 지 몰라서 변환이라고 했습니다.. 환승 레지스터도 괜찮은 것 같은데
이 레지스터들은 정점 셰이더에서 픽셀 셰이더로 데이터를 넘기는 데 쓰입니다. 넘겨지는 데이터는 GPU가 알맞게 보간하므로 픽셀 셰이더 측에서는 올바른 픽셀 값을 받아 처리할 수 있습니다.

정점 색깔이나 텍스쳐링을 위한 UV 좌표가 이런 방식으로 넘겨지는 전형적인 데이터입니다.

이 레지스터들에는 v<n>으로 접근할 수 있습니다. <n>은 레지스터 숫자입니다.

쓸 수 있는 변환 레지스터는 8개입니다.

6. 텍스쳐 추출기(sampler) 레지스터
텍스쳐 추출기 레지스터는 UV 좌표에 기반해 텍스쳐에서 색상값을 뽑는 데 쓰입니다.

추출에 쓸 텍스쳐는 액션스크립트에서 Context3D::setTextureAt()를 호출하여 설정합니다.

텍스쳐 추출기를 이용하기 위한 문법은 fs<n> <flags>입니다. <n>은 추출기 인덱스, <flags>는 추출 방식을 기술하는 하나 이상의 플래그입니다.

<flags>는 다음의 것들을 정의하는, 콤마로 구분된 문자열의 집합입니다.

텍스쳐 차원. 선택: 2d, cube
밉 매핑. 선택: nomip(또는 mipnoone. 둘은 똑같다), mipnearst, miplinear
텍스쳐 필더링. 선택: nearest, linear
텍스쳐 반복. 선택: repeat, wrap, clamp 

예를 들어 MIP 매핑과 선형 필터링 없는 표준 2D 텍스쳐는 다음과 같이 추출해 임시 레지스터 ft1 안에 넣을 수 있습니다.

tex ft1, v0, fs0 <2d,linear,nomip>
 
위 예제에서 변환 레지스터 v0은 보간된 텍스쳐 UV를 담습니다.

견본 AGAL 셰이더 만들기

이 절에서는 셰이더 예제를 하나 보며, 여러분은 이 예제가 어떻게 작동하는지 더 잘 이해하게 될 것입니다.

Vertex Buffer의 정점들이 오프셋 0에 정점 위치를, 오프셋 3에 정점 색상을 포함한다고 가정합시다. 코드는 이럴 것입니다.

var vertices:Vector.<Number> = Vector.<Number>([
-0.3,-0.3,0, 1, 0, 0, // x, y, z, r, g, b
-0.3, 0.3, 0, 0, 1, 0,
0.3, 0.3, 0, 0, 0, 1]);
 
정점 셰이더가 정점 위치를 올바르게 변환하고 픽셀 셰이더에 각 정점 색깔을 올바르게 넘기는 것이 목표입니다. 

이 일을 다음과 같이 할 수 있습니다.

m44 op, va0, vc0 // pos to clipspace
mov v0, va1 // 색깔을 복사한다
 
첫 번째 줄은 정점과 입력 속성 레지스터 0간에 4x4 행렬 곱셈을 수행합니다. 변환 행렬은 액션스크립트로부터 온 것입니다. 원근법에 따라 렌더링할 때 이 행렬은 대개 모델 공간에서 클립 공간으로 변환하는 행렬이며, 액션스크립트로부터 상수 레지스터 0 즉 vc0으로 넘어왔다고 가정합니다. 

주의: 클립 공간과 원근 투영은 이 시리즈의 다음 튜토리얼에서 보다 자세히 설명할 것입니다. 제목은 Stage3D 그리고 원근 투영과 일하기입니다.

다음 호출로 행렬을 레지스터 vc0으로 넘길 수 있습니다.

Context3D::setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, matrix, true );
 
정점 셰이더의 두 번째 줄은 정점 색상 데이터를 변환 레지스터 0 즉 v0으로 복사합니다. GPU가 이 데이터를 보간해서 픽셀 셰이더에 넘깁니다.

픽셀 셰이더는 그저 변환 레지스터 v0의 색상값을 출력 레지스터 0c로 복사합니다.
 
mov oc, v0

즉 이 정점/픽셀 셰이더 짝은 그저 3D 모델 정점을 액션스크립트에서 넘어온 변환 행렬을 써서 변환하고 정점 색상을 적용할 뿐입니다.

이것입니다! 여러분의 첫 번째 정점 셰이더와 픽셀 셰이더입니다.

Program3D와 AGAL 미니 어셈블러를 사용하여 견본 액션스크립트 애플리케이션 제작하기

그러면 이 AGAL 코드를 어떻게 액션스크립트 애플리케이션에 넣을까요? 여기서 Stage3D API가 나옵니다.

var program:Program3D = context3D.createProgram();

렌더링을 위해 Program3D(셰이더)를 쓰려면 이것을 먼저 GPU에 업로드해야 합니다. 다음 메서드를 호출하면 됩니다.

Program3D::upload(vertexByteCode: ByteArray, fragmentByteCode:ByteArray);

이 메서드는 입력으로서 정점 셰이더와 단편(픽셀) 셰이더 버전의 컴파일된 목적 코드를 요구합니다.

AGAL 미니 어셈블러를 쓰는 것이 AGAL 셰이더를 컴파일하는 좋은 방법입니다. 이 어셈블러는 정점 셰이더와 픽셀 셰이더 소스 코드를 문자열로 입력받아 실행 시간에 목적 코드로 컴파일하는 도구입니다.

여기서 AGAL 미니 어셈블러를 내려받을 수 있습니다.
http://www.bytearray.org/wp-content/projects/agalassembler/com.zip 

그럼 위에서 설명한 대로 먼저 AGAL 미니 어셈블러로 정점 셰이더와 픽셀 셰이더를 컴파일합니다.

var vertexShaderAssembler : AGALMiniAssembler = new AGALMiniAssembler();
vertexShaderAssembler.assemble( Context3DProgramType.VERTEX,
"m44 op, va0, vc0\n" + // pos to clipspace
"mov v0, va1" // copy color
);
var fragmentShaderAssembler : AGALMiniAssembler= new AGALMiniAssembler();
fragmentShaderAssembler.assemble( Context3DProgramType.FRAGMENT,
"mov oc, v0"
);
 
그리고나면 GPU에 정점/픽셀 셰이더 프로그램을 업로드합니다. 

var program:Program3D = context3D.createProgram(); 
program.upload( vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode);
 
액션스크립트를 써서 AGAL과 통신하기 

셰이더는 그 자체로 살지 않습니다. 셰이더는 여러분의 Stage3D 기반 액션스크립트 애플리케이션에서 사용됩니다. 따라서 애플리케이션은 처리할 데이터를 셰이더에 넘길 수 있어야 합니다.

보통은 셰이더에 VertexBuffer 데이터(정점 속성), Texture, 그 외 변환 행렬 같은 추가적인 매개변수가 필요합니다.

실행 시간에 Program3D 그리고 관련 VertexBuffer와 Texture를 사용하기 전에 다음 호출로 이것들을 활성화해야 합니다.

Contex3D::setProgram(program:Program3D)
Context3D::setVertexBufferAt(index:int, buffer:VertexBuffer3D, bufferOffset:int, format:String)
Context3D::setTextureAt(sampler:int, texture:TextureBase)

setVertexBufferAt은 정점 버퍼의 특정 오프셋(세 번째 매개변수)에 있는 한 정점 속성을 활성화하고, 인덱스로 기술되는(첫 매개변수) 속성 레지스터 (스트림)과 연결한다는 것에 주의하세요.

setTextureAt 호출은 Texture를 활성화하고 첫 매개변수 "sampler"로 기술되는 특정 텍스처 추출기와 연결합니다.

렌더링 직전에 이렇게 호출합니다.

// 정점 위치를 속성 레지스터 0에 저장한다
context3D.setVertexBufferAt (0, vertexbuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
// 텍스처 추출기 0에 텍스처를 할당한다
context3D.setTextureAt( 0, texture );
// 셰이더 프로그램을 할당한다
context3D.setProgram( program );

이것들 중 무엇이든 활성화하기에 앞서 각각에 대응하는 업로드 메서드를 호출하여 GPU에 업로드했는지 확인해야 합니다.

Program3D::upload()
VertexBuffer3D::uploadFromVector(data:Vector.<Number>, startVertex:int, numVertices:int)
Texture::uploadFromBitmapData(source:BitmapData, miplevel:uint = 0)

이제 셰이더에 매개변수를 넘겨 상수 레지스터에 저장되게 하고 싶으실 것입니다. 다음과 같이 호출합니다.

Context3D::setProgramConstantsFromVector(programType:String, firstRegister:int, data:Vector.<Number>, numRegisters:int = -1)
Context3D::setProgramConstantsFromMatrix(programType:String, firstRegister:int, matrix:Matrix3D, transposedMatrix:Boolean = false)

각각 액션스크립트 Vector와 Matrix3D를 기록합니다.

그러니까 셰이더에 기본적인 회전 행렬을 넘기려면 이렇게 할 수 있습니다.

var m:Matrix3D = new Matrix3D();
m.appendRotation(getTimer()/50, Vector3D.Z_AXIS);
context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, m, true);
 
행렬은 정점 셰이더가 사용할 수 있게 상수 레지스터 0에 저장됩니다.

이제 어디로 가야 하나

이번 글에서는 셰이더와 AGAL 셰이딩 언어를 소개했습니다. Stage3D API에 기반해 액션스크립트 3D 애플리케이션을 제작하고자 하면 셰이더를 만드는 것은 필수 단계입니다. 아직은 완벽히 작동하는 Stage3D 애플리케이션을 만들지 않았지만 이 글의 내용은 Stage3D API를 쓰기 위한 중요한 기반입니다. 다음 글에서는 모든 것을 한데 모아, Stage3D로 간단한 기하 구조를 그리는 방법을 보여줍니다.

Comments