http://blog.naver.com/frustum/150008165621
이 문서의 저작권은 김용준( http://3dstudy.net )에게 있습니다.
상업적인 이용을 금지합니다.
제목 : ‘DirectX 9.0 셰이더 프로그래밍’
2부 고수준 셰이더 언어(High Level Shader Language)
DirectX 8.0부터 소개된 셰이더는 그 강력한 능력을 빠르게 3D프로그래밍 기술의 주류시장을 잠식해 들어가고 있다. 그러나, 8.0에서의 셰이더 프로그래밍은 불편한 어셈블러였기 때문에 개발자들을 상당히 힘들게했던 것이 사실이다. 이번에 DirectX 9.0에 포함된 HLSL을 사용해서 고수준의 셰이더를 제작해 보도록 하자.
1장 문법
셰이더는 기본적으로 C언어를 기반으로 하고 있으므로, C언어에 대한 이해를 필수이다. 여기서는 독자들이 C언어를 이미 알고있다고 가정하고 설명을 진행해 나가도록 하겠다. 그러나, C언어를 잘 모르는 독자라도 베이직등의 기본언어를 한번이라도 익혀본 독자라면 HLSL을 이해하는데 별로 무리가 없을 것이다. HLSL의 문법은 그만큼 쉽기 때문이다.
1절 자료형(data types)
HLSL은 많은 자료형을 지원하는데, 간단한 불린, 정수, 부동소수부터 벡터, 행렬, 구조체 처럼 확장된 자료형까지도 지원한다.
우리가 이번에 살펴볼 내용은 다음과 같다.
• 스칼라 형(Scalar Types)
• 변수 선언(Declaring a Variable)
• 형 변경자(Type Modifiers)
• 지억부류 변경자(Storage Class Modifiers)
• 벡터 형(Vector Types)
• 행렬 형(Matrix Types)
• 객체 형(Object Types)
• 구조체 형(Structure Types)
• 사용자정의 형(User-Defined Types)
• 형 변환(Type Casts)
• 변수(Variables)
• 구성성분 접근과 뒤섞기(Component Access and Swizzles)
1.1 스칼라 형(Scalar Types)
다음과 같은 스칼라 형을 지원한다.
bool : true 혹은 false
int : 32비트 부호있는 정수
half : 16비트 부동소수
float : 32비트 부동소수
double : 64비트 부동소수
모든 타겟 플랫폼이 정수형을 지원하지는 않기 때문에 하드웨어에서 float를 사용해서 에뮬레이션 될 수도 있으며, 만약 정수형의 범위를 넘어서는 값이 있다면 이 값을 사용한 함수는 정확하게 작동하지 않을 수도 있다. 마찬가지로 half와 double역시 지원되지 않을 경우에는 float를 사용하여 에뮬레이션 된다.
1.2 변수 선언(Declaring a Variable)
C언어와 마찬가지로 변수를 선언한다. 다음은 fVar이라는 부동소수 변수를 선언한 것이다.
float fVar;
특정한 값으로 선언과 동시에 초기화를 할 수도 있다.
float fVar = 3.1f;
배열형태로 선언하는 것도 가능하다.
int iVar[3];
배열 선언과 동시에 초기화는 다음과 같이 한다.
int iVar[3] = {1,2,3};
1.3 형 변경자(Type Modifiers)
형 변경자는 변수형의 바로앞에 붙어서 컴파일러에 추가적인 정보를 제공해주는 선택적 명령어이다. 다음과 같은 명령어가 있다.
• const
• row_major
• col_major
1.3.1 const
const는 변수가 상수임을 나타내는 명령어로서 셰이더 실행중에 값이 바뀌지 않는 다는 것을 나타낸다. 변수를 const로 선언하면 컴파일러는 변수를 쓰기접근이 불가능한 메모리 영역에 넣는다. 당연히 값을 바꿀 수 없으므로, 선언과 동시에 초기화가 되어야 한다.
const float fConstant = 0.2f;
프로그램에 의해서 변화가 가능한 셰이더 상수는 static변경자를 사용하지 않은 전역변수이다. static에 대한 자세한 내용은 '1.4 지억부류 변경자(Storage Class Modifiers)'를 참고하자.
1.3.2 row_major, col_major
행렬성분은 행위주(row-major)나 열위주(column-major)방식으로 구성된다. 열위주란 행렬의 한 열이 하나의 상수레지스터에 보관되는 방식이며, 행위주는 행렬의 각 행이 하나의 레지스터에 보관되는 방식이다.
row_major float4x4 worldMatrix;
행위주 행렬은 다음과 같이 놓이게 된다.
11 12 13 14
21 22 23 24
31 32 33 34
41 42 43 44
col_major float4x4 transposedWorldMatrix;
열위주 행렬은 다음과 같이 놓이게 된다.
11 21 31 41
12 22 32 42
13 23 33 43
14 24 34 44
row_major와 column_major명령어는 행렬이 상수테이블이나 셰이더 입력으로부터 읽혀질때만 효력이 있으며, 행렬성분이 HLSL코드안에서 사용되는 데에는 아무런 효력이 없다.
1.4 지억부류 변경자(Storage Class Modifiers)
기억부류 변경자는 변수의 생성소멸시점과 접근범위를 컴파일러에게 지정해주는 역할을 한다. 단, 셰이더 전연벽수는 셰이더의 가장 앞머리에서 선언되어야 한다.(함수의 바깥쪽)
float globalShaderVariable;
void function()
{
float localShaderVariable;
...
}
HLSL은 다음과 같은 기억부류 변경자를 지원한다.
• static
• extern
• uniform
• shared
1.4.1 static
전역범위볼때 static은 다른 프로그램에서 이 변수에 접근할 수 없도록 한다. GetVertexShaderConstantx()나 SetVertexShaderConstantx(), ID3DXConstantTable을 통해서 이 변수에 접근 할 수 없다.
static float fConstant_Hidden_From_the_App = 0.2f;
지역범위에서 볼때(함수의 내부에 선언될때) static은 다르게 해석되는데, 처음 함수가 실행되고 나서도 그 값이 계속 유지된다는 의미이다.
1.4.2 extern
extern은 static의 반대되는 의미이다. extern변수는 셰이더의 바깥에서 값이 설정되어야 한다. 전역변수는 extern과 static이 동시에 선언될 수 없다.
extern float4 fogColor;
extern변수에 값을 설정하기 위해서는 SetVertexShaderConstantx()나 ID3DXConstantTable을 사용한다. 전역변수가 extern이나 static중 어느쪽도 지정되지 않았다면, 컴파일러는 extern으로 가정한다.
1.4.3 uniform
uniform변수는 API함수를 사용해서 화면에 그려질때만 값이 바뀔수 있다.
uniform float fConstant_Between_Draw_Calls = 0.2f;
이 말은 일단 값이 설정되면, 모든 정점들(정점셰이더)이나 픽셀들(픽셀셰이더)에서 초기설정값이 일정하게 계속 유지된다는 뜻이다.
1.4.4 shared
effect들간에 공유될 전역변수에는 shared를 사용한다.
shared float sharedAlphaValue
1.5 벡터 형(Vector Types)
벡터형은 하나 혹은 4개의 구성성분를 가진 특별한 자료 구조체이다.
bool bVector; // 1개의 Boolean값을 가진 스칼라
bool1 bVector; // 1개의 Boolean값을 가진 벡터
int1 iVector; // 1개의 int값을 가진 벡터
half2 hVector; // 2개의 half값을 가진 벡터
float3 fVector; // 3개의 float값을 가진 벡터
double4 dVector; // 4개의 double값을 가진 벡터
자료형의 바로뒤에 붙은 숫자가 벡터의 구성성분 개수를 나타낸다.
물론, 선언과 동시에 초기화도 가능하다.
bool bVector = false;
int1 iVector = 1;
half2 hVector = { 0.2, 0.3 };
float3 fVector = { 0.2f, 0.3f, 0.4f };
double4 dVector = { 0.2, 0.3, 0.4, 0.5 };
vector라는 명령어를 사용해서 동일한 선언을 할수도 있다.
vector <bool, 1> bVector = false;
vector <int, 1> iVector = 1;
vector <half, 2> hVector = { 0.2, 0.3 };
vector <float, 3> fVector = { 0.2f, 0.3f, 0.4f };
vector <double, 4> dVector = { 0.2, 0.3, 0.4, 0.5 };
vector형은 <>을 사용해서 자료형과 구성성분의 개수를 지정한다.
1.5.1 벡터 구성성분 접근하기
벡터는 4개의 구성성분을 가질수 있는데, 각각의 성분은 2가지 명명집합(naming set)을 사용해서 접근할 수 있다.
• 위치값 방식: x,y,z,w
• 컬러값 방식: r,g,b,a
다음의 소스는 모두 3번째 벡터성분의 값을 읽어온다.
float4 pos = float4(0,0,2,1);
pos.z // 값은 2
pos.b // 값은 2
명명집합은 한 개 이상의 성분을 사용할 수 있지만, 섞을 수는 없다.
float4 pos = float4(0,0,2,1);
float2 temp;
temp = pos.xy // 유효
temp = pos.rg // 유효
temp = pos.xg // 오류! 위치값 방식과 컬러값 방식은 섞어서 사용할 수 없다.
1.5.2 벡터 구성성분 뒤섞기(swizzle)
값을 읽을 때 하나 혹은 그 이상의 구성성분을 지정하는 것을 뒤섞기라고 한다.
float4 pos = float4(0,0,2,1);
float2 f_2D;
f_2D = pos.xy; // 2개의 값을 읽는다
f_2D = pos.xz; // 어떠한 순서로 읽어도 상관없다
f_2D = pos.zx;
f_2D = pos.xx; // 같은 값을 한번이상 읽는것도 가능하다
f_2D = pos.yy;
1.5.3 벡터 구성성분 마스킹
마스킹은 몇개의 값이 쓰여질 것인지를 조절한다.
float4 pos = float4(0,0,2,1);
float4 f_4D;
f_4D = pos; // 4개의 성분을 쓴다
f_4D.xz = pos.xz; // 2개의 성분을 쓴다
f_4D.zx = pos.xz; // 쓰기 순서를 바꾼다
f_4D.xzyw = pos.w; // 하나의 성분을 여러 개에 쓸 수도 있다
f_4D.wzyx = pos;
다음처럼 하나의 값에 두개의 값을 쓸수는 없다.
f_4D.xx = pos.xy; // 오류!
구성성분의 명명집합을 섞어서 사용할 수 없다.
f_4D.xg = pos.rgrg; // 오류!
1.5.4 벡터 수학
HLSL은 표준수학 표기법과 약간 다른데, 그 이유는 연산자가 구성 성분별로 정의되기 때문이다. 예를들어서 float4 v = a*b 는 다음문장과 동일한 문장이다.
float4 v;
v.x = a.x*b.x;
v.y = a.y*b.y;
v.z = a.z*b.z;
v.w = a.w*a.w;
이것은 4개의 구성성분 곱셈이며, 내적이 아니다. 내적은 •(a,b)로 표기한다. 두 연산의 차이를 비교해 보면 다음과 같다.
// 4개성분 벡터 곱셈의 첫번째 성분
a.x * b.x = a.x*b.x
// 4개성분 내적의 첫번째 성분
a.x • b.x = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
행렬도 구성성분별 연산을 사용한다.
float3x3 mat1,mat2;
...
float3x3 mat3 = mat1*mat2;
이것은 행렬곱이 아니라, 두 행렬의 구성성분별 곱셈이다.
// 구성성분별 행렬 곱셈
mOne._m00 * mTwo._m00 = mOne._m00 * mTwo._m00;
행렬곱은 mul()이라는 내부함수를 사용한다.
float3x3 mat1,mat2;
...
float3x3 mat3 = mul(mat1,mat2);
다음은 두 행렬의 적(product)이다.
// 4개성분 행렬 곱셈의 첫번째 성분
mul(mOne._m00, mTwo._m00) = mOne._m00 * mTwo._m00 + mOne._m01 * mTwo._m10
+ mOne._m02 * mTwo._m20 + mOne._m03 * mTwo._m30;
중복지정된 mul()함수는 벡터와 행렬간의 곱셈을 수행한다.
예) vector * vector, vector * matrix, matrix * vector, matrix * matrix
다음 소스는 동일한 역할을 한다.
float4x3 World;
float4 main(float4 pos : POSITION) : POSITION
{
float4 val;
val.xyz = mul(pos,World);
val.w = 0;
return val;
}
float4x3 World;
float4 main(float4 pos : POSITION) : POSITION
{
float4 val;
val.xyz = (float3) mul((float1x4)pos,World);
val.w = 0;
return val;
}
이 예제는 pos벡터를 (float1x4)형변환을 사용해서 열벡터로 변환한다. 벡터를 형변환하거나, mul()함수에 전달되는 인수의 순서를 바꾸는 것은 행렬을 전치(transpose)하는 것과 같다.
자동형변환의 결과로 다음의 예제는 mul()과 dot()함수가 같은 결과를 반환하게 된다.
{
float4 val;
return mul(val,val);
}
mul()의 결과는 1x4 * 4x1 = 1x1벡터이다.
{
float4 val;
return dot(val,val);
}
내적의 반환값은 하나의 스칼라 값이다.
1.6 행렬 형(Matrix Types)
행렬은 행과 열을 가진 자료형이다. 행렬의 구성성분은 어떠한 스칼라 값도 가능하지만, 모든 구성성분은 동일한 자료형이어야 한다. 행과 열의 개수는 "행x열"의 형태로 자료형의 뒤에 추가된다.
int1x1 iMatrix; // 1행1열의 int행렬
int2x1 iMatrix; // 2행1열의 int행렬
...
int4x1 iMatrix; // 4행1열의 int행렬
...
int1x4 iMatrix; // 1행4열의 int행렬
double1x1 dMatrix; // 1행1열의 double행렬
double2x2 dMatrix; // 2행2열의 double행렬
double3x3 dMatrix; // 3행3열의 double행렬
double4x4 dMatrix; // 4행4열의 double행렬
단, 행과 열의 최대 값은 4이며, 최소값은 1이다.
행렬은 선언과 동시에 초기화 될 수 있다.
float2x2 fMatrix = { 0.0f, 0.1, // 1행
2.1f, 2.2f // 2행 };
혹은 matrix라는 명령어를 사용해서 동일한 선언을 할 수도 있다.
matrix <float, 2, 2> fMatrix = { 0.0f, 0.1, // 1행
2.1f, 2.2f // 2행 };
이 예제는 부동소수형의 2행 2열짜리 행렬을 생성한 것이다.
matrix형도 vector와 마찬가지로 <>을 사용해서 자료형과 구성성분의 개수를 지정한다.
다음 선언은 2행 3열의 half자료형 행렬을 정의한다.
matrix <half, 2, 3> fHalfMatrix;
1.6.1 행렬성분 접근
행렬의 성분은 구조체 연산자인 "."을 통해서 접근가능한데, 2가지의 명명집합을 지원한다.
첨자가 0부터 시작하는 방식
_m00, _m01, _m02, _m03
_m10, _m11, _m12, _m13
_m20, _m21, _m22, _m23
_m30, _m31, _m32, _m33
첨자가 1부터 시작하는 방식
_11, _12, _13, _14
_21, _22, _23, _24
_31, _32, _33, _34
_41, _42, _43, _44
각각의 명명집합은 밑줄("_") 바로뒤에 행과 열의 숫자를 적어주면 된다. 첨자가 0부터 시작하는 명명집합은 앞에 "m"자를 붙여준다.
2개의 명명집합을 사용한 접근방식 예제를 보도록 하자.
float2x2 fMatrix = { 1.0f, 1.1f, // 1행
2.0f, 2.1f // 2행 };
float f_1D;
f_1D = matrix._m00; // 1행 1열의 값 : 1.0
f_1D = matrix._m11; // 2행 2열의 값 : 2.1
f_1D = matrix._11; // 1행 1열의 값 : 1.0
f_1D = matrix._22; // 2행 2열의 값 : 2.1
벡터와 마찬가지로, 명명집합은 하나이상의 성분을 동시에 사용할 수 있다.
float2x2 fMatrix = { 1.0f, 1.1f, // 1행
2.0f, 2.1f // 2행 };
float2 temp;
temp = fMatrix._m00_m11 // 유효
temp = fMatrix._m11_m00 // 유효
temp = fMatrix._11_22 // 유효
temp = fMatrix._22_11 // 유효
1.6.2 행렬의 배열접근
행렬은 첨자가 0으로 시작하는 배열표기법을 사용해서 접근할 수도 있다. 4x4행렬은 배열방식을 사용하면 다음과 같이 구성되어 있다.
[0][0], [0][1], [0][2], [0][3]
[1][0], [1][1], [1][2], [1][3]
[2][0], [2][1], [2][2], [2][3]
[3][0], [3][1], [3][2], [3][3]
행렬에 접근하는 예제를 보도록 하자.
float2x2 fMatrix = { 1.0f, 1.1f, // 1행
2.0f, 2.1f // 2행 };
float temp;
temp = fMatrix[0][0] // 성분 하나 읽기
temp = fMatrix[0][1] // 성분 하나 읽기
구조체 연산자인 "."이 사용되지 않았다는 것을 명심하자. 배열표기법은 하나이상의 값을 읽기위해서 뒤섞기(swizzle)를 사용할 수 없다.
float2 temp;
temp = fMatrix[0][0]_[0][1] // 오류! 2개의 성분을 읽을수 없다.
그러나, 배열접근은 다중성분벡터를 읽을 수 있다.
float2 temp;
float2x2 fMatrix;
temp = fMatrix[0] // 첫번째 행을 읽는다
1.6.3 행렬요소 뒤섞기(Swizzling)
벡터처럼, 행렬도 하나이상의 성분을 읽는 것을 뒤섞기라고 한다.
float4x4 worldMatrix = float4( {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3} );
float4x4 tempMatrix;
float2 tempFloat;
다음과 같은 대입은 유효한 대입연산이다.
tempMatrix._m00_m11 = worldMatrix._m00_m11; // 다중 성분
tempMatrix._m00_m11 = worldMatrix.m13_m23;
tempMatrix._11_22_33 = worldMatrix._11_22_33; // 뒤섞기 순서도 마음대로
tempMatrix._11_22_33 = worldMatrix._24_23_22;
1.6.4 행렬성분 마스킹
몇개의 성분이 쓰여질 것인지를 제어하는 것을 마스킹이라고 한다.
float4x4 worldMatrix = float4( {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3} );
float4x4 tempMatrix;
tempMatrix._m00_m11 = worldMatrix._m00_m11; // 2개의 성분 쓰기
tempMatrix._m23_m00 = worldMatrix.m00_m11;
다음처럼 하나의 성분에 2개의 성분을 대입할 수 없다.
tempMatrix._m00_m00 = worldMatrix.m00_m11; // 오류!
다음처럼 명명집합을 섞어서 사용할 수 없다.
tempMatrix._11_m23 = worldMatrix._11_22; // 오류!
1.6.5 행렬 순서
행렬결합순서(matrix packing order)는 uniform인 경우 기본적으로 열위주(column-major)방식이다. 이 말은 행렬의 각 열이 하나의 상수 레지스터에 보관된다는 뜻이다. 행위주(row-major) 행렬결합은 각행이 하나의 상수레지스터에 저장된다.
행렬결합은 #pragma pack_matrix를 사용하거나, row_major, col_major명령을 사용해서 바꿀 수 있다.
일반적으로 열위주 행렬이 행위주 행렬보다 효율적인데, 다음 예제를 보면 이유를 알 수 있을 것이다.
<HLSL코드>
// 열위주 행렬결합
float4x3 World;
float4 main(float4 pos : POSITION) : POSITION
{
float4 val;
val.xyz = mul(pos,World);
val.w = 0;
return val;
}
<컴파일한 어셈블리 코드 >
vs_2_0
def c3, 0, 0, 0, 0
dcl_position v0
m4x3 oPos.xyz, v0, c0
mov oPos.w, c3.x
// 4개의 명령슬롯 사용
[표 1-1] 열위주 결합
<HLSL코드>
// 행위주 행렬결합
#pragma pack_matrix(row_major)
float4x3 World;
float4 main(float4 pos : POSITION) : POSITION
{
float4 val;
val.xyz = mul(pos,World);
val.w = 0;
return val;
}
<컴파일한 어셈블리 코드 >
vs_2_0
def c4, 0, 0, 0, 0
dcl_position v0
mul r0.xyz, v0.x, c0
mad r2.xyz, v0.y, c1, r0
mad r4.xyz, v0.z, c2, r2
mad oPos.xyz, v0.w, c3, r4
mov oPos.w, c4.x
// 5개의 명령슬롯 사용
[표 1-2] 행위주 결합
2개의 어셈블리 코드를 비교해 보면 알 수 있지만, 열위주 행렬결합방식을 사용하면 명령슬롯이 더 절약되며, 사용하는 상수 레지스터의 개수도 적다.
1.7 객체 형(Object Types)
HLSL에서 지원하는 객체 형은 다음과 같은 것들이 있다.
• 샘플러(sampler)
• 구조체(structure)
• 문자열(string)
• 정점셰이더 객체(vertex shader object)
• 픽셀셰이더 객체(pixel shader object)
• 텍스처(texture)
1.7.1 샘플러(sampler)
샘플러는 샘플러 상태(sampler state)를 포함하고 있는데, 샘플러 상태란 샘플링될 텍스처를 정하고, 샘플링중에 사용할 필터링 방법을 정하는 것으로 다음의 3가지가 필요하다.
• 텍스처
• 샘플러
• 샘플링 명령
다음은 2차원 텍스처 샘플링을 하는 예제이다.
texture tex0;
sampler2D s_2D;
float2 sample_2D(float2 tex : TEXCOORD0) : COLOR
{
return tex2D(s_2D, tex);
}
텍스처는 texture형의 tex0라는 이름으로 선언되며, 이 예제에서, s_2D라는 샘플러변수는 sampler2D형과 sampler_state명령으로 선언되었다. 샘플러는 샘플러 상태를 {}안에 포함하게 되는데, 여기에는 샘플링될 텍스처와 랩핑모드, 필터모드 등의 필터상태가 포함된다. 만약, 샘플러 상태가 생략되면, 기본 상태 선형필터링과 랩모드가 적용된다.
다음은 기본 샘플러 상태와 필터, 텍스처 주소 모드를 선택한 예이다.
texture tex0;
sampler3D s_3D;
float3 sample_3D(float3 tex : TEXCOORD0) : COLOR
{
return tex3D(s_3D, tex);
}
이것은 입방체 맵핑 텍스처 샘플러이다.
texture tex0;
samplerCUBE s_CUBE;
float3 sample_CUBE(float3 tex : TEXCOORD0) : COLOR
{
return texCUBE(s_CUBE, tex);
}
샘플러 상태 설정 중에서 밉맵필터를 선형(LINEAR)로 설정한 것이다.
sampler s = sampler_state { texture = NULL; mipfilter = LINEAR; };
마지막으로 1차원 샘플러를 보도록 하자.
texture tex0;
sampler1D s_1D;
float sample_1D(float tex : TEXCOORD0) : COLOR
{
return tex1D(s_1D, tex);
}
DirectX 런타임이 1차원 텍스처를 지원하지 않기 때문에, 컴파일러는 y좌표을 무시하는 2차원 텍스처를 생성할 것이다. 결국, tex1D가 2차원 텍스처 탐색방법을 사용하기 때문에 컴파일러는 y좌표를 효율적으로 다루는 방법을 선택하게 된다. 조금 드문 경우지만, 컴파일러가 y항을 효율적으로 처리하는 방법을 선택할 수 없으면 경고를 발생시킨다.
다음의 특별한 예제는 컴파일러가 입력 좌표를 다른 레지스터로 옮겨야 하기 때문에 비효율적이다. (1차원 텍스처 검색은 2차원 검색으로 이루어지는데, 텍스처 좌표가 float1으로 선언되었기 때문이다.)만약 코드가 float2로 쓰여져 있다면 컴파일러는 y좌표를 뭔가 적절하게 초기화해서 텍스처 좌표를 사용할 수 있을 것이다.
texture tex0;
sampler s_1D_float;
float4 main(float texCoords : TEXCOORD) : COLOR
{
return tex1D(s_1D_float, texCoords);
}
모든 텍스처 검색에는 bias나 proj가 추가될수 있다.(즉, tex2Dbias 혹은 texCUBEproj같은 방식). proj가 접미사가 붙으면 텍스처좌표는 w-성분으로 나눠지게 되고, bias가 붙으면 밉맵 단계가 w성분에 의해서 이동한다. 즉, 접미사가 붙은 모든 텍스처 검색은 float4형식의 입력이 필요하다는 것이다. tex1D와 tex2D는 각각 yz요소와, z요소를 무시한다.
샘플러는 또한 배열형태로 사용될 수도 있는데, 아직까지는 샘플러의 동적배열 형태를 지원하지는 않는다. 즉, tex2D(s[0],tex)는 컴파일 시에 값을 결정 할 수 있으므로 유효하지만, tex2D(s[a],tex)는 불가능하다는 얘기다. 샘플러의 동적 접근은 주로 루프를 다루는 코드 작성시에 매우 유용하다. 다음 코드를 보도록 하자.
sampler sm[4];
float4 main(float4 tex[4] : TEXCOORD) : COLOR
{
float4 retColor = 1;
for(int i = 0; i < 4;i++)
{
retColor *= tex2D(sm[i],tex[i]);
}
return retColor;
}
1.7.2 구조체(structure)
struct명령어는 구조체형을 정의한다.
다음 구조체는 위치값과 법선값, 2개의 멤버를 포함한다.
struct vertexData
{
float3 pos;
float3 normal;
};
모든 HLSL기본 자료형은 구조체에서 사용할 수 있으며, 멤버변수에 접근하기 위해서는 "."연산자를 사용한다.
struct vertexData data = { { 0.0, 0.0, 0.0 },
{ 1.1, 1.1, 1.1 }
};
data.pos = float3(1,2,3);
data.pos = {1,2,3};
float3 temp = data.normal;
구조체가 일단 정의되면, struct명령어가 없어도 이름에 의해서 참조할 수 있다.
vertexData main(vertexData in)
{
... // 어쩌구 저쩌구
return in;
};
구조체 멤버는 초기값을 가질수 없으며, 또한 static, extern, volatile, const등의 접근범위 명령어를 사용해서 개별적으로 선언될 수 없다.
1.7.3 문자열(String)
아스키 문자열, 문자열을 받아들이는 연산이나 상태는 없다. 문자열과 주석문은 effect에서만 사용될 수 있다.
1.7.4 정점셰이더 객체(Vertex Shader Object)
vertexshader 자료형은 정점셰이더 객체를 나타내는데, HLSL셰이더가 컴파일 되거나, 어셈블리 셰이더가 어셈블될때 대입된다.
<어셈블리 셰이더>
vertexshader vs =
asm
{
vs_2_0
dcl_position v0
mov oPos, v0
};
<HLSL 셰이더 >
vertexshader vs=compile vs_2_0 vsmain();
1.7.5 픽셀셰이더 객체(Pixel Shader Object)
pixelshader 자료형은 정점셰이더 객체를 나타내는데, HLSL셰이더가 컴파일 되거나, 어셈블리 셰이더가 어셈블될때 대입된다.
<어셈블리 셰이더>
pixelshader ps =
asm
{
ps_2_0
mov oC0, c0
};
<HLSL 셰이더 >
pixelshader ps = compile ps_2_0 psmain();
1.7.6 텍스처(Texture)
texture자료형은 텍스처 객체를 나타낸다. 이 자료형은 effect에서 텍스처를 디바이스에 설정하기 위해서 사용된다.
texture tex0;
이 정의는 다음과 같은 의미를 갖는다.
• 텍스처 형
• 변수명은 tex0
일단 텍스처변수가 선언되면, 샘플러에 의해서 참조될 수 있다.
texture tex0;
sampler Sampler_1D;
{
texture = (tex0);
};
1.8 구조체 형(Structure Types)
struct명령어는 구조체 형을 정의할때 사용하며, 구조체가 정의되면 ID를 통해서 접근할 수 있다.
struct [id] { member_list }
member_list는 하나 이상의 멤버 선언으로 구성되는데, 초기값을 가질수 없으며, 또한 static, extern, volatile, const등의 접근범위 명령어를 사용해서 개별적으로 선언될 수 없다.
1.9 사용자정의 형(User-Defined Types)
typedef라는 명령어는 형의 이름을 정의할 때 사용하는데, const를 사용해서 상수형 임을 명시할 수도 있다. 배열 접미사는 각 ID뒤에 붙을 수 있으며, 일단 형이 선언되면, ID를 사용해서 참조할 수 있다.
typedef [const] type id [array_suffix] [, id ...] ;
배열접미사는 하나이상의 [정수]형태로 구성되어, 차원을 나타낸다.
D3DX 8.0과의 호환성을 위해서 다음과 같은 형들은 전역범위로 대소문자 구분없이 미리 선언되어 있다.
typedef int DWORD;
typedef float FLOAT;
typedef vector <float, 4> VECTOR;
typedef matrix <float, 4, 4> MATRIX;
typedef string STRING;
typedef texture TEXTURE;
typedef pixelshader PIXELSHADER;
typedef vertexshader VERTEXSHADER;
편의를 위해서 다음과 같은 형들도 전역범위로 미리 선언되어 있다. 여기서 #은 1~4 사이의 정수값이다.
typedef vector <bool, #> bool#;
typedef vector <int, #> int#;
typedef vector <half, #> half#;
typedef vector <float, #> float#;
typedef vector <double, #> double#;
typedef matrix <bool, #, #> bool#x#;
typedef matrix <int, #, #> int#x#;
typedef matrix <half, #, #> half#x#;
typedef matrix <float, #, #> float#x#;
typedef matrix <double, #, #> double#x#;
1.10 형 변환(Type Casts)
다음과 같은 형 변환이 지원된다.
스칼라-스칼라 : 항상 가능. 불린형을 정수나 부동소수로 형변환 할 경우, false는 0, true는 1로 간주한다. 정수나 부동소수를 불린으로 형변환 할 경우에는 0이 false, 0이 아닌값이 true이다. 부동소수를 정수로 형변환 할 때는 소수점 이하는 0으로 짤린다.
스칼라-벡터 : 항상 가능. 스칼라 값을 벡터에 구성요소들에 복제해 넣는다.
스칼라-행렬 : 항상 가능. 스칼라 값을 벡터에 구성요소들에 복제해 넣는다.
스칼라-객체 : 불가능
스칼라-구조체 : 구조체의 모든 멤버가 숫자라면 가능. 스칼라 값을 벡터에 구성요소들에 복제해 넣는다.
벡터-스칼라 : 항상 가능. 벡터의 첫번째 성분을 선택한다.
벡터-벡터 : 대상벡터가 원본벡터보가 크면 안된다. 가장 좌측에 있는 값들부터 보관하고 나머지를 자른다. 이 형변환을 위해서 행-행렬(column matrix)과 열-행렬(row matrix), 수치 구조체는 벡터로 다뤄진다.
벡터-행렬 : 벡터의 크기가 행렬의 크기와 일치해야 한다.
벡터-객체 : 불가능
벡터-구조체 : 구조체가 벡터보다 크지 않으며, 구조체의 구성요소가 수치형이면 가능.
행렬-스칼라 : 항상 가능. 행렬의 좌측상단을 선택한다.
행렬-벡터 : 행렬의 크기가 벡터의 크기와 일치해야 한다.
행렬-행렬 : 두개의 차원 모두 대상 행렬이 원본행렬보다 크면 안된다. 좌측상단의 값들부터 보관하고, 나머지를 버린다.
행렬-객체 : 불가능
행렬-구조체 : 구조체의 크기가 행렬과 같아야 하며, 구조체의 모든 구성요소가 수치형이어야 한다.
객체-스칼라 : 불가능
객체-벡터 : 불가능
객체-행렬 : 불가능
객체-객체 : 객체형이 동일하면 가능
객체-구조체 : 구조체는 하나이상의 멤버를 갖고 있으면 안된다. 구조체 멤버의 형이 객체와 동일해야 한다.
구조체-스칼라 : 구조체는 적어도 하나의 수치형 멤버를 가져야 한다.
구조체-벡터 : 구조체는 적어도 벡터의 크기여야 한다. 첫번째 구성요소는 벡터의 크기보다 큰 수치형이어야 한다.
구조체-행렬 : 구조체는 적어도 행렬의 크기여야 한다. 첫번째 구성요소는 행렬으 크기보다 큰 수치형이어야 한다.
구조체-객체 : 구조체는 적어도 하나의 멤버를 포함하고 있어야 한다. 구조체의 멤버는 객체와 동일해야 한다.
구조체-구조체 : 대상구조체는 원본구조체보다 크면 안된다. 각각의 원본과 대상 구성요소간의 형변환 가능성에 달려있다.
1.11 변수(Variables)
변수는 다음과 같이 선언한다.
[static uniform extern shared volatile] [const]
type id [array_suffix] [: semantic] [= initializers] [annotations] [, id ...] ;
변수 선언 앞에 static명령어를 붙일 수 있는데, 전역변수에 붙으면 이 변수가 셰이더 외부에서 접근할수 없다는 뜻이고, 지역변수에 사용되면 함수호출뒤에서 계속 값이 유효하다는 것이다. 지역 static변수는 오직 한번만 초기화되며, 명시적인 초기값이 없다면 0으로 가정한다.
• 전역 변수 선언 앞에 uniform을 붙이면, 셰이더에게 uniform입력임을 나타내주는 것이며, 모든 static이 아닌 전역변수는 uniform이다.
• 전역 변수 선언 앞에 extern을 붙이면, 외부입력 변수라는 뜻이며, 모든 static이 아닌 전역변수는 extern이다.
• 전역 변수 선언 앞에 shared를 붙이면, effect프레임워크로 하여금 effect들간에 공유되는 변수로 다루도록 해준다.
• 전역 변수 선언 앞에 volatie을 붙이면, effect프레임워크에 이 변수가 매우 빈번하게 바뀐다는 것을 알려주는 것이다.
초기값은 표현식이나 { expression [, expression [, ...]] }형태 일수 있는데, 전역 extern변수는 리터럴(literal)값이어야 하며, 다른 전역변수나 static지역변수는 상수이어야 한다.
변수는 의미자(semantic)가 붙을 수 있는데, 의미자는 언어적으로는 아무런 의미가 없지만,
변수와 결합하여 하드웨어에 전달된다. 대소문자 구별도 없다. 의미자의 효용성과 의미는 사용자가 정의하는 기능에 달려있다. 예를 들어, 정점셰이더라면 입력과 출력 레지스터를 하드웨어에 맵핑하는데 사용된다.
전역 변수는 설명문(annotation)을 가질 수 있는데, 설명문은 { member_list }형태로 되어 있으며, 각 member_list의 멤버들은 리터럴 값으로 초기화 된다. 설명문은 effect와 전달인자를 교환하기위한 방법일 뿐이며, 프로그램안에서 참조할 수 없다. 또한 설명문의 멤버변수들은 의미자를 가질 수 없다.
1.12 구성성분 접근과 뒤섞기(Component Access and Swizzles)
기본 자료형의 부동소수 성분은 다음의 이름표 첨자를 사용해서 구조체의 멈버변수 처럼 각각 접근할수 있다.
_11, x, r _12, y, g _13, z, b _14, w, a
_21 _22 _23 _24
_31 _32 _33 _34
_41 _42 _43 _44
벡터의 특정 성분들은 두 세개의 첨자를 조합하여 접근 할 수 있다.
bgr, yyzw, _12_22_32_42
모든 이름들은 같은 첨자 집합을 사용해야 한다. 첨자집합은 섞어서 사용할 수 없으나, 같은 성분을 연속해서 사용할 수는 있다.
2절 문장과 식(statements & expressions)
HLSL에서 식(expression)은 변수와 리터럴, 연산자의 나열이며, 문장(statement)은 평가(evaluation)될 식의 순서를 결정한다.
2.1 식(Expression)
식은 리터럴과 변수를 연산자로 조합한 것이다.
변수 : 리터럴 리터럴이란 정수를 나타내는 숫자 1이나 , 부동소수를 나타내는 숫자 2.1처럼 명시된 자료값을 말하며, 주로 변수에 값을 대입할 때 사용된다
연산자 :
- 1절의 자료형 항목을 참고
- 연산자란 리터럴 혹은 변수들이 어떻게 결합, 비교,선택 될 것인지를 결정하며, 다음과 같은 연산자들이 있다.
- 대입 =, +=, -=, *=, /=
- 단항 !, -, +
- 산술연산 +, -, *, /, %
- 불린 &&, ||, ?:
- 비교 <, >, ==, !=, <=, >=
- 전위,후위 ++, --
- 형변환 (type)
- 콤마 ,
- 구조체멤버 선택 .
- 배열멤버 선택 [i]
[표 2-1] 식에 사용되는 리터럴,변수,연산자
많은 수의 연산자들이 성분별(per component)로 지원된다. 즉, 변수의 각 구성성분별로 연산이 수행된다는 얘기다. 예를 들어서, 구성성분이 하나뿐인 변수는 연산이 한번만 일어나지만, 4개짜리 변수는 4번 연산이 일어나게 된다.
2.1.1 대입연산자(Assignment Operators)
연산자 : =, +=, -=, *=, /=
변수에는 리터럴 값이 대입될 수 있다.
int i = 1;
half h = 3.0;
float f2 = 3.1f;
bool b = false;
string str = "string";
변수에는 수학적 연산의 결과값이 대입될 수 있다.
int i1 = 1;
i1 += 2; // i1 = 1 + 2 = 3
변수는 등호기호(=)의 양쪽에 사용될 수 있다.
float f3 = 0.5f;
f3 *= f3; // f3 = 0.5 * 0.5 = 0.25
부동소수로 나누면 나머지 연산에 아무런 문제가 생기지 않는다.
float f1 = 1.0;
f1 /= 3.0f; // f1 = 1.0/3.0 = 0.333
그러나, 나누어질 숫자가 정수일 경우에는 주의를 요한다. 특히 버림값이 결과에 영향을 미칠때는 말이다. 다음 예제는 앞의 예제와 같지만, 자료형이 다르기 때문에, 버림에 의해서 전혀 다른 결과가 나오게 된다.
int i1 = 1;
i1 /= 3; // i1 = 1/3 = 0.333, 소수점 이하를 버리므로 결국 0
2.1.2 단항 연산자(Unary Operators)
연산자 : !, -, +
단항 연산자는 단일한 피연산자에 대해서 작용한다.
bool b = false;
bool b2 = !b; // b2 = true
int i = 2;
int i2 = -i; // i2 = -2
int j = +i2; // j = +2
2.1.3 산술 연산자(Additive & Multiplicative Operators)
연산자 : +, -, *, /, %
int i1 = 1;
int i2 = 2;
int i3 = i1 + i2; // i3 = 3
i3 = i1 * i2; // i3 = 1 * 2 = 2
i3 = i1/i2; // i3 = 1/3 = 0.333. i3이 정수이므로 0으로 버려짐
i3 = i2/i1; // i3 = 2/1 = 2
float f1 = 1.0;
float f2 = 2.0f;
float f3 = f1 - f2; // f3 = 1.0 - 2.0 = -1.0
f3 = f1 * f2; // f3 = 1.0 * 2.0 = 2.0
f3 = f1/f2; // f3 = 1.0/2.0 = 0.5
f3 = f2/f1; // f3 = 2.0/1.0 = 2.0
잉여 연산자(modulus operator) %는 나눗셈의 나머지 값을 반환한다.
이 연산의 결과는 정수와 부동소수일 때의 결과가 다르다.
int i1 = 1;
int i2 = 2;
i3 = i1 % i2; // i3 = 1/2의 나머지는 1
i3 = i2 % i1; // i3 = 2/1의 나머지는 0
i3 = 5 % 2; // i3 = 5/2의 나머지는 1
i3 = 9 % 2; // i3 = 9/2의 나머지는 1
f3 = f1 % f2; // f3 = 1.0/2.0의 나머지는 0.5
f3 = f2 % f1; // f3 = 2.0/1.0의 나머지는 0.0
% 연산자는 양변이 양수이거나 양변이 음수일 때만 정의되며, C와는 다르게, 정수, 부동소수 모두에 대해서 작동한다.
2.1.4 불린 연산자(Boolean Math Operators)
연산자 : &&, ||, ?:
bool b1 = true;
bool b2 = false;
bool b3 = b1 && b2 // b3 = true AND false = false
b3 = b1 || b2 // b3 = true OR false = true
C언어는 &&, ||, ?: 평가 시에 short-circuit을 사용 하는 반면, HLSL은 벡터연산이기 때문에 short-circuit을 사용 하지 않고, 양변의 식이 항상 모두 평가 된다.
<여기서 잠깐> short-circuit
short-circuit이란 한쪽 변을 먼저 평가하여 결과를 내는 방식이다.
( a || b ) 라는 경우를 생각해보자. 만약 a가 TRUE라면 b의 값이 무엇이던 ( a || b )는 항상 TRUE일 것이다. 반대로 ( a && b )를 생각해보면, a가 FALSE라면 b의 값과 무관하게 ( a && b )는 반드시 FALSE이다. 이처럼 한쪽 변의 결과값을 기준으로 빠르게 판정을 내리는 방식을 short-circuit이라 한다.
</여기서 잠깐>
불린 연산자는 구성요소별로 작동한다. 예를들어, 2개의 벡터를 비교한다면, 이 연산의 결과는 각 구성요소들을 비교한 결과값이 들어있는 벡터이다.
불린 연산자를 사용하는 식은 각 변수의 크기와 구성요소의 형이 같도록 늘어난다. 늘어나는 형은 연산이 발생할 크기와 식의 결과값에 따르게 되는데, 예를 들어서 int3 + float식은 float3+float3로 늘어나게 되고, 결과값도 float3가 된다.
2.1.5. 비교 연산자(Comparison Operators)
연산자 : <, >, ==, !=, <=, >=
스칼라 값의 크거나(작거나)를 비교한다.
if( dot(lightDirection, normalVector) > 0 )
// 면에 조명처리
if( dot(lightDirection, normalVector) < 0 )
// 면이 뒷면
혹은, 스칼라 값의 같음(같지 않음)을 비교한다.
if(color.a == 0)
// 면이 보지이 않으므로 처리할 것 없음
if(color.a != 0)
// 알파값으로 두개의 색을 섞음
혹은, 스칼라 값의 크거나 같음(작거나 같음)을 비교한다.
if( position.z >= oldPosition.z )
// 새로운 면이 현재 면보다 뒤에 있으므로 처리 생략
if( currentValue <= someInitialCondition )
// 현재 값을 초기상태로 되돌림
어떠한 스칼라 자료형도 비교 가능하다. 그러나, 비교 연산자는 벡터,행렬,객체형 등의 복합자료형(complex data type)은 지원하지 않는다.
2.1.6 전위, 후위 연산자(Prefix, Postfix Operators)
연산자 : ++, --
전위 연산자는 식이 평가되기 전에 변수의 값을 바꾸고, 후위연산자는 식을 평가한 다음에 값을 바꾼다.
다음 예제는 반복횟수를 세기 위해서 반복문에서 i값을 사용한다.
float4 arrayOfFloats[4] = { 1.0f, 2.0f, 3.0f, 4.4f };
for (int i = 0; i < 4; )
{
arrayOfFloats[i++] *= 2;
}
후위 증가 연산자 ++가 사용되었으므로, arrayOfFloats[i]가 i값이 증가하기 전에 2가 곱해진다. 이것을 전위연산자로 똑같이 구현하면 약간 복잡해 진다.
float4 arrayOfFloats[4] = { 1.0f, 2.0f, 3.0f, 4.4f };
for (int i = 0; i < 4; )
{
arrayOfFloats[++i - 1] *= 2;
}
전위 증가 연산자 ++가 사용되었으므로, arrayOfFloats[i+1 - 1]는 i가 증가한 후에 2가 곱해진다.
전위 감소 와 후위 감소 연산자 --도 증가 연산자와 똑같이 사용하면 된다. 다만, 증가 연산자는 1을 더하는데 반해서 후위연산자는 1을 감소 시킨다는 점이 다를 뿐이다.
2.1.7 형변환 연산자(Cast Operator)
괄호안에 있는 자료형의 명은 명시적 형변환이다.
형변환은 원래의 식을 변환하려는 형으로 변환한다.
일반적으로 간단한 자료형은 복합 자료형으로 형변환 될 수 있다. – 승격 형변환(promotion cast)
그러나, 일부분의 복합 자료형만이 간단한 자료형으로 형변환 될 수 있다. – 격하 형변환(demotion cast)
형변환이 가능한 경우에 대해서는 1절을 참고하도록 하자.
2.1.8 콤마 연산자(Comma Operator)
콤마 연산자는 하나이상의 식을 분할하여 순서대로 평가되도록 하며, 마지막 식의 값이 전체 식의 값이 된다.
매우 재미있는 예제를 하나 보도록 하자.
1. 다음 식에서 실수로 등호(=) 우측의 float4를 빼버리면 어떻게 될까?
// 여기서 우측의 float4는 생성자 역할을 한다.
float4 x = float4(0,0,0,1);
2. 아마도, 다음과 같은 모양이 될 것이다. 문제는 여기서부터 발생한다.
컴파일러는 (0,0,0,1)을 단순히 콤마가 3개인 식으로 평가하게 된다.
float4 x = (0,0,0,1);
3. 콤마 연산자는 왼쪽에서 오른쪽으로 식을 평가한다. 결국 최종적으로 다음의 식으로 변화된다. 맙소사!
float4 x = 1;
4. HLSL은 이 경우 스칼라 승격을 사용한다. float4 x의 구성요소에 모두 1을 넣는 것이다. 결국 다음 소스와 동일한 코드가 된다.
float4 x = float4(1,1,1,1);
이 예제에서 보다시피, 등호 우측의 float4를 빼는 것은 결과적으로 치명적인 오류가 된다. 왜냐면 float4 x = (0,0,0,1); 도 분명히 문법적으로 적법한 문장이기 때문이다.
2.1.9 구조체 연산자(Structure Operator)
연산자 : .
struct position
{
float4 x;
float4 y;
float4 z;
};
이것은 다음과 같이 사용한다.
struct position pos = { 1,2,3 };
float 1D_Float = pos.x
1D_Float = pos.y
각각의 멤버는 구조체 연산자를 사용해서 읽고 쓸 수 있다.
struct position pos = { 1,2,3 };
pos.x = 2.0f;
pos.z = 1.0f; // z = 1.0f
pos.z = pos.x // z = 2.0f
2.1.10 배열 연산자(Array Operator)
연산자 : [i]
배열 멤버 선택 연산자 [i]는 배열에서 하나 혹은 그 이상의 배열요소를 선택한다. 이때, []안의 첨자는 0부터 시작하는 정수이다.
int arrayOfInts[4] = { 0,1,2,3 };
arrayOfInts[0] = 2;
arrayOfInts[1] = arrayOfInts[0];
배열 연산자를 벡터에 접근할때도 사용된다.
float4 4D_Vector = { 0.0f, 1.0f, 2.0f, 3.0f };
float 1DFloat = 4D_Vector[1]; // 1.0f
추가적인 첨자를 붙여서 행렬에 접근할 수도 있다.
float4x4 mat4x4 = {{0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3} };
mat4x4[0][1] = 1.1f;
float 1DFloat = mat4x4[0][1]; // 0.0f
첫번째 첨자는 0부터 시작하는 행첨자이며, 두번째 첨자는 0부터 시작하는 렬첨자 이다.
2.1.11 연산자 우선순위
연산자 사용 방법 의미 결합법칙
() (value) 부분식 왼쪽에서 오른쪽
() id(arguments) 함수의 호출 왼쪽에서 오른쪽
type(arguments) 형 생성자 왼쪽에서 오른쪽
[] array[int] 배열의 첨자 왼쪽에서 오른쪽
. structure.id 멤버의 선택 왼쪽에서 오른쪽
value.swizzle 성분의 교체 왼쪽에서 오른쪽
++ variable++ 후위 증가 (성분마다) 왼쪽에서 오른쪽
-- variable-- 후위 감소 (성분마다) 왼쪽에서 오른쪽
++ ++variable 전위 증가 (성분마다) 오른쪽에서 왼쪽
-- --variable 전위 감소 (성분마다) 오른쪽에서 왼쪽
! ! value 논리 NOT (성분마다) 오른쪽에서 왼쪽
- -value 단항 마이너스 (성분마다) 오른쪽에서 왼쪽
+ +value 단항 플러스 (성분마다) 오른쪽에서 왼쪽
() (type) value 형 변환 오른쪽에서 왼쪽
* value*value 곱셈 (성분마다) 왼쪽에서 오른쪽
/ value/value 나눗셈 (성분마다) 왼쪽에서 오른쪽
% value%value 잉여 (성분마다) 왼쪽에서 오른쪽
+ value+value 덧셈 (성분마다) 왼쪽에서 오른쪽
- value-value 뺄셈 (성분마다) 왼쪽에서 오른쪽
< value < value 비교 : 보다 작은 (성분마다) 왼쪽에서 오른쪽
> value > value 비교 : 보다 큰 (성분마다) 왼쪽에서 오른쪽
<= value <= value 비교 : 이하 (성분마다) 왼쪽에서 오른쪽
>= value >= value 비교 : 이상 (성분마다) 왼쪽에서 오른쪽
== value == value 비교 : 동일한 (성분마다) 왼쪽에서 오른쪽
!= value != value 비교 : 동일하지 않은 (성분마다) 왼쪽에서 오른쪽
&& value && value 논리 AND (성분마다) 왼쪽에서 오른쪽
|| value||value 논리 OR (성분마다) 왼쪽에서 오른쪽
? : float? value:value 조건 오른쪽에서 왼쪽
= variable=value 대입 (성분마다) 오른쪽에서 왼쪽
*= variable*=value 곱셈 대입 (성분마다) 오른쪽에서 왼쪽
/= variable/=value 나눗셈 대입 (성분마다) 오른쪽에서 왼쪽
%= variable%=value 잉여 대입 (성분마다) 오른쪽에서 왼쪽
+= variable+=value 덧셈 대입 (성분마다) 오른쪽에서 왼쪽
-= variable-=value 뺄셈 대입 (성분마다) 오른쪽에서 왼쪽
, value, value 콤마 왼쪽에서 오른쪽
2.2 문장(statement)
문장 블럭은 하나 이상의 문장으로 이루어진 그룹으로서, {}으로 감싼 블럭이다.
{
statement 1;
statement 2;
...
statement n;
}
문장 블럭이 단일문장이라면 {}을 써도 되고 안써도 된다.
또한 if문 다음에 한 줄짜리 문장이 온다면, {}으로 감싸는 것은 전적으로 개인의 자유이다.
if( some expression)
color.rgb = tex3D(Sampler, texturecoordinates);
다음의 코드는 앞의 코드와 완전히 동일하다.
if( some expression)
{
color.rgb = tex3D(Sampler, texturecoordinates);
}
단, 블록 안에 선언된 변수는 그 블록 안에서만 유효하다는 것을 잊지 말자.
2.2.1 return 문(Return Statement)
return문은 함수의 끝을 나타낸다.
다음 코드는 가장 간단한 return문으로, 제어권을 호출자에게로 넘겨준다. 이 코드는 아무런 값도 반환하지 않는다.
void main()
{
return ;
}
return 문은 하나 이상의 값을 반환 할 수 있는데, 다음 코드는 리터럴 값을 반환하는 예이다.
float main( float input : COLOR0) : COLOR0
{
return 0;
}
다음 예제는 식의 스칼라 값을 반환한다.
return light.enabled = true ;
다음 예제는 지역변수와 리터럴 값을 사용해서 생성한 float4를 반환한다.
return float4(color.rgb, 1) ;
다음 예제는 내부 함수로부터 반환된 값과 리터럴값으로 생성한 float4를 반환한다.
float4 func(float2 a: POSITION): COLOR
{
return float4(sin(length(a) * 100.0) * 0.5 + 0.5, sin(a.y * 50.0), 0, 1);
}
다음 예제는 하나 이상의 멤버를 가진 구조체를 반환한다.
float4x4 WorldViewProj;
struct VS_OUTPUT
{
float4 Pos : POSITION;
};
VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
VS_OUTPUT out;
out.Pos = mul(inPos, WorldViewProj );
return out;
};
2.2.2 흐름제어 문(Flow-Control Statements)
흐름제어 문은 다음 번에 실행된 문장이 어느 것인지를 결정 것으로, if, do for, while등이 있다.
2.2.2.1 if문
if문은 비교의 결과값에 근거해서 다음 번에 실행될 문장을 정한다.
if ( (Normal dot LightDirection) > 0 )
{
// 면에 조명처리, 확산광 색깔을 더할 것
...
}
문장 블럭은 하나 이상의 문장으로 이루어 졌으며 {}으로 감싼 영역을 말하는데, 셰이더 명령슬롯의 크기를 넘지만 않는다면 얼마든지 커도 상관없다.
if문은 선택적으로 else블럭을 가질 수 있는데, if식이 참이라면 if문과 연계되어 있는 블럭이 처리되고, 그렇지 않다면 else에 연계된 블럭이 처리된다.
2.2.2.2 do 문
do문은 문장블럭을 실행 한 뒤 상태식(conditional expression)을 평가해서 문장블럭을 다시 실행할지 여부를 결정한다. 이 과정은 상태식의 값이 false가 될 때까지 반복된다.
do
{
// one or more statements
color /= 2;
} while ( color.a > 0.33f )
앞의 코드는 색깔 성분들을 2로 나누고나서 알파성분은 검사해서 반복할지 여부를 결정한다. 알파성분이 0.33f보다 크다면 색깔값의 성분들을 계속 반으로 나누는 과정을 반복하다가 알파성분이 0.33f보다 작아지면 비교식의 값이 거짓이 되어, 프로그램은 다음 번 명령으로 진행하게 된다.
이 코드는 한 줄짜리 문장블럭을 사용했지만, 다음과 같이 여러 줄짜리 문장 블럭을 사용해도 된다.
do {
color.r /= 2;
color.g /= 4;
color.b /= 8;
.... // Other statements
color.a /= 8;
} while ( color.a > 0.33f )
이 코드에서 알파값을 변화시키는 것을 잊으면 안된다. 만약 그걸 까먹었다가는 이 코드가 무한루프에 빠질 것이고, 셰이더 실행에 막대한 지장을 줄 것이다.
2.2.2.3 for문
for문은 정해진 횟수만큼 문장블럭을 반복하는 정적인 제어를 제공해 준다.
for문에는 초기식, 비교식, 증가식이 포함되며 문장블럭이 뒤따른다.
for ( int i = 0; i < 2; i++)
{
// 하나 이상의 문장블럭
...
문장 n;
};
다음 예제는 4개의 다른 텍스처 좌표에서 텍스처를 읽어들이기 위해서 for반복문을 사용 하였다.
sampler RenderTarget;
float4 textureCoordinates[4];
float4 outColor[4];
for(int i = 0; i < 3; i++)
{
outColor[i] = tex2D(RenderTarget, textureCoordinates[i]);
}
문장 블럭은 비교식이 true일 때마다 반복해서 실행된다.
2.2.2.4 while문
while문은 식을 평가하여 문장블럭을 실행할지 여부를 판단해서 반복한다.
while ( color.a > 0.33f )
{
color /= 2;
}
이 코드는 알파성분이 0.33f보다 큰지 확인한 다음, 참이면 절반으로 나눈다. 알파값이 0.33f보다 작아지면 비교식이 거짓이 되어 프로그램은 다음 문장을 실행하게 된다.
다음 코드는 한줄짜리 문장이며, 한줄짜리 문장은 {}을 사용할지 여부를 선택할 수 있다.
while ( color.a > 0.33f )
color /= 2;
문장 블럭은 n개로 확장할 수 있다.
while ( color.a > 0.33f )
{
color.r /= 2;
color.g /= 4;
color.b /= 8;
.... // Other statements
color.a /= 8;
}
알파값을 바꾸는 것을 잊지 말자. 무한루프에 빠질 수 있다.
3절 셰이더 의미자(shader semantics)부터는 다음에....
3절 셰이더 의미자(shader semantics)
3절에서는 다음과 같은 내용들을 살펴 볼 것이다.
• 정점셰이더 의미자(Vertex Shader Semantics)
• 픽셀셰이더 의미자(Pixel Shader Semantics)
• 명시적 레지스터 결합(Explicit Register Binding)
3.1 정점 셰이더 의미자(Vertex Shader Semantics)
HLSL 의미자의 역할은 셰이더 데이터의 입력과 출력을 식별하는 것이다. 의미자는 3가지 경우에 나타난다.
• 구조체 멤버 뒤에
• 함수의 입력 전달인자 리스트들 중의 전달인자 뒤에
• 함수의 입력 전달인자 리스트 뒤에
다음 예제는 하나이상의 셰이더 입력을 위해서 구조체를 사용한다. 그리고, 셰이더 출력을 위해서 다른 구조체를 사용하는데, 이들 구조체 멤버들이 의미자를 사용하고 있다.
vector vClr;
struct VS_INPUT
{
float4 vPosition : POSITION;
float3 vNormal : NORMAL;
float4 vBlendWeights : BLENDWEIGHT;
};
struct VS_OUTPUT
{
float4 vPosition : POSITION;
float4 vDiffuse : COLOR;
};
float4x4 mWld1;
float4x4 mWld2;
float4x4 mWld3;
float4x4 mWld4;
float Len;
float4 vLight;
float4x4 mTot;
VS_OUTPUT VS_Skinning_Example(const VS_INPUT v, uniform float len=100)
{
VS_OUTPUT out;
// 스킨 위치(월드 공간으로)
float3 vPosition =
mul(v.vPosition, (float4x3) mWld1) * v.vBlendWeights.x +
mul(v.vPosition, (float4x3) mWld2) * v.vBlendWeights.y +
mul(v.vPosition, (float4x3) mWld3) * v.vBlendWeights.z +
mul(v.vPosition, (float4x3) mWld4) * v.vBlendWeights.w;
// 스킨 법선(월드 공간으로)
float3 vNormal =
mul(v.vNormal, (float3x3) mWld1) * v.vBlendWeights.x +
mul(v.vNormal, (float3x3) mWld2) * v.vBlendWeights.y +
mul(v.vNormal, (float3x3) mWld3) * v.vBlendWeights.z +
mul(v.vNormal, (float3x3) mWld4) * v.vBlendWeights.w;
// 출력할 것들
out.vPosition = mul(float4(vPosition + vNormal * Len, 1), mTot);
out.vDiffuse = dot(vLight,vNormal);
return out;
}
입력 구조체는 정점버퍼로부터 셰이더 입력이 될 데이터를 식별한다.
앞서 소개한 코드의 셰이더는 정점버퍼로부터 들어오는 입력을 POSITION, NORMAL, BLENDWEIGHT요소로 구별해서 셰이더 레지스터에 맵핑(연결)한다.
HLSL의 입력 데이터는 정점선언 데이터형과 완벽하게 일치할 필요는 없는데, 데이터형이 맞지 않을 경우, HLSL에서 지정된 형으로 자동변환 되기 때문이다. 예를 들자면, 프로그램에서 UINT로 정의된 NORMAL형은 셰이더에서 읽혀질때는 float3형으로 변환된다는 것이다.
정점 스트림의 데이터가 셰이더에 선언된 데이터형보다 적을 경우, 없는 성분들은 0으로 초기화된다. 단 w성분은 예외적으로 1로 초기화 된다.
입력 의미자는 고정함수 파이프라인의 D3DDECLUSAGE 열거형 값과 비슷하다고 생각하면 된다.
출력 구조체는 정점 셰이더 출력 인자를 식별한다. 이 예제의 경우에는 위치값과 색깔값을 사용한다. 이들 출력값은 삼각형 레스터라이즈에 사용될 것인데, POSITION이라고 표기된 것이 스크린 좌표계에서의 좌표이다. 스크린 좌표계에서는 -1과 +1이 뷰포트에서의 x,y좌표의 최소,최대 값이며, z값은 z버퍼 테스트에 사용된다. 그리고, 어떠한 정점셰이더도 반드시 POSITION값만큼은 출력해야 한다.
출력 의미자도 고정함수 파이프라인의 D3DDECLUSAGE 열거형 값과 비슷하다고 생각하면 된다. 일반적으로 정점셰이더의 출력 구조체는 픽셀 셰이더의 입력 구조체로 사용된다.
픽셀셰이더의 경우에는 POSITION, PSIZE, FOG으로 연결된 값들은 읽을 수가 없는데, 만약, 이들 값들이 필요할 경우에는 픽셀 셰이더의 다른 출력 레지스터에 복사하여 의미자를 다른 형식(TEXCOORDn등)으로 사용하면 된다.
3.1.1 정점 셰이더 입력 의미자(Vertex Shader Input Semantics)
다음과 같은 입력 의미자가 있다.
POSITION[n] 위치(position)
BLENDWEIGHT[n] 결합가중치(blend weight)
BLENDINDICES[n] 결합인덱스(blend index)
NORMAL[n] 법선(normal)
PSIZE[n] 점크기(point size)
COLOR[n] 확산,반사광(diffuse specular color)
TEXCOORD[n] 텍스처 좌표(texture coordinate)
TANGENT[n] 접선(tangent)
BINORMAL[n] 종법선(binormal)
TESSFACTOR[n] 분할 인수(tessellation factor)
n은 0부터 시작하는 정수로서 지원되는 레지스터의 숫자이다. 예) PSIZE0, COLOR1, 등
3.1.2 정점 셰이더 출력 의미자(Vertex Shader Output Semantics)
다음과 같은 입력 의미자가 있다.
POSITION 위치(position)
PSIZE 점크기(point size)
FOG 정점 안개(vertex fog)
COLOR[n] 색깔(Color) 예) COLOR0
TEXCOORD[n] 텍스처 좌표(texture coordinate) 예) TEXCOORD0
n은 0부터 시작하는 정수로서 지원되는 레지스터의 숫자이다. 예) TEXCOORD0, TEXCOORD1, 등
3.2 픽셀 셰이더 의미자(Pixel Shader Semantics)
HLSL 의미자의 역할은 셰이더 데이터의 입력과 출력을 식별하는 것이다. 의미자는 3가지 경우에 나타난다.
• 구조체 멤버 뒤에
• 함수의 입력 전달인자 리스트들 중의 전달인자 뒤에
• 함수의 입력 전달인자 리스트 뒤에
다음 예제는 정점셰이더 출력과 픽셀셰이더 입력으로 같은 구조체를 사용한다. 픽셀셰이더는 색깔값을 반환한다는 것을 나타내기 위해서, 함수의 전달인자의 뒤에 의미자를 사용하고 있다.
struct VS_OUTPUT
{
float4 Position : POSITION;
float3 Diffuse : COLOR0;
float3 Specular : COLOR1;
float3 HalfVector : TEXCOORD3;
float3 Fresnel : TEXCOORD2;
float3 Reflection : TEXCOORD0;
float3 NoiseCoord : TEXCOORD1;
};
float4 PixelShader_Sparkle(VS_OUTPUT In) : COLOR
{
float4 Color = (float4)0;
float4 Noise = tex3D(SparkleNoise, In.NoiseCoord);
float3 Diffuse, Specular, Gloss, Sparkle;
Diffuse = In.Diffuse * Noise.a;
Specular = In.Specular;
Specular *= Specular;
Gloss = texCUBE(Environment, In.Reflection) * saturate(In.Fresnel);
Sparkle = saturate(dot((saturate(In.HalfVector) - 0.5) * 2,
(Noise.rgb - 0.5) * 2));
Sparkle *= Sparkle;
Sparkle *= Sparkle;
Sparkle *= Sparkle * k_s;
Color.rgb = Diffuse + Specular + Gloss + Sparkle;
Color.w = 1;
return Color;
}
VS_OUTPUT으로부터 들어오는 모든 입력은 의미자를 가지고 있다. 이들 값들은 정점셰이더로부터 반환된 값이므로, 픽셀셰이더 입력으로 읽혀진다. 다른 3개의 입력은 프로그램에서 설정된 전역 uniform변수들인 Environment, SparkleNoise, k_s값으로, Environment와 SparkleNoise는 모두 프로그램에 의해서 생성되고 설정되는 텍스처들이며, k_s도 프로그램에 의해서 설정된 상수 레지스터이다.
전역 변수들은 컴파일러에 의해서 자동적으로 레지스터에 할당된다. 전역변수들은 uniform전달인자로도 불리우는데, 그것은 이들 변수의 값이 셰이더가 호출될 때 처리되는 픽셀들에 대해서 항상 일정(uniform)하기 때문이다.
상수테이블에 있는 레지스터들은 ID3DXConstantTable를 통해서 접근 가능하다.
픽셀 셰이더의 입력 의미자는 정점셰이더와 픽셀셰이더의 값을 특정한 하드웨어에 연결(mapping)하게 되는데, 레지스터마다 특별한 성질이 있다. 현재까지는 오직 TEXCOORD와 COLOR의미자만이 유효하기 때문에 전달하려는 값이 텍스처좌표가 아니라 하더라도 TEXCOORD 의미자를 사용해야 한다.
정점셰이더의 출력 구조체에는 POSITION이 있지만, 픽셀셰이더 입력에서는 이 값이 없음을 유념하자. HLSL은 픽셀셰이더에서 유효하지 않은 값이라도 정점셰이더에서 유효한 출력값이면 이를 허용하는데, 결과적으로는 픽셀셰이더에서 참조 되지 않는다.
입력 인자는 배열일 수도 있다. 이 경우 의미자는 각 배열요소마다 자동적으로 컴파일러에 의해서 증가된다. 예를 들어 다음과 같은 명시적 선언을 생각해보자.
struct VS_OUTPUT
{
float4 Position : POSITION;
float3 Diffuse : COLOR0;
float3 Specular : COLOR1;
float3 HalfVector : TEXCOORD3;
float3 Fresnel : TEXCOORD2;
float3 Reflection : TEXCOORD0;
float3 NoiseCoord : TEXCOORD1;
};
float4 Sparkle(VS_OUTPUT In) : COLOR
여기서 이루어진 명시적 선언은, 다음과 같이 선언할 경우 컴파일러에 의해서 의미자가 자동적으로 붙기 때문에 동일한 선언이 된다.
float4 Sparkle(float4 Position : POSITION,
float3 Col[2] : COLOR0,
float3 Tex[4] : TEXCOORD0) : COLOR0
{
// shader statements
...
입력 의미자처럼, 출력 의미자도 픽셀셰이더 출력데이터를 구분해 주는데, 대부분의 픽셀 셰이더는 출력값을 그냥 COLOR0로 하게 된다.
또한, 픽셀 셰이더는 DEPTH0값과 여러개(최대 4개)의 렌더타겟도 가질수 있다. 다음 셰이더 코드는 색깔값과 깊이값을 모두 출력하고 있다.
struct PS_OUTPUT
{
float4 Color[4] : COLOR0;
float Depth : DEPTH;
};
PS_OUTPUT main(void)
{
PS_OUTPUT out;
// 셰이더 문
...
// 4개의 출력색깔 값을 쓴다.
out.Color[0] = ...
out.Color[1] = ...
out.Color[2] = ...
out.Color[3] = ...
// Write pixel depth
out.Depth = ...
return out;
}
픽셀셰이더의 출력 색깔값은 float4형식만 가능하다. 여러가지 색깔값을 사용하고 싶다면, 연속적으로만 사용 가능한데, 이 말은 COLOR1을 사용하고 싶다면 먼저 COLOR0값을 출력해야만 한다는 뜻이다. 그리고, 픽셀셰이더에서 출력하는 깊이값의 경우에는 float1형만 가능하다.
3.2.1 픽셀 셰이더 입력 의미자(Pixel Shader Input Semantics)
다음과 같은 의미자를 지원한다.
COLOR[n] 확산,반사광 색깔 예) COLOR0, COLOR1
TEXCOORD[n] 텍스처 좌표 예) TEXCOORD0
n은 0부터 시작하는 정수로서 지원되는 레지스터의 숫자이다. 예) TEXCOORD0, TEXCOORD1, 등
3.2.2 픽셀 셰이더 출력 의미자(Pixel Shader Output Semantics)
다음과 같은 의미자를 지원한다.
COLOR[n] 색깔 예) COLOR0
TEXCOORD[n] 텍스처좌표 예) TEXCOORD0
DEPTH[n] 깊이값 예) DEPTH0
n은 0부터 시작하는 정수로서 지원되는 레지스터의 숫자이다. 예) COLOR0, TEXCOORD1, DEPTH0, 등
3.3 명시적 레지스터 결합(Explicit Register Binding)
우리는 이미 컴파일러가 전역변수에 레지스터를 자동적으로 할당한다는 것을 알고있다. 그러나, 변수에 특정한 레지스터를 결합하도록 명시하는 것도 가능하다.
다음과 같은 3개의 전역변수가 있을 경우,
sampler Environment;
sampler SparkleNoise;
float4 k_s;
컴파일러는 자동적으로 Environment에 샘플러 레지스터 s0를, SparkleNoise에 샘플러 레지스터 s1를 할당하고, k_s에 상수레지스터 c0를 할당할 것이다.(이전에 할당된 레지스터들이 없다고 가정하자)
그러나, 컴파일러에게 특정한 레지스터를 할당하도록 하려면 register(...)구문을 사용하면 된다.
sampler Environment : register(s1);
sampler SparkleNoise : register(s0);
float4 k_s : register(c12);
이 경우 Environment는 s1샘플러 레지스터에, SparkleNoise는 s0샘플러 레지스터에, 마지막으로 k_s는 c12상수 레지스터에 할당될 것이다.
4절 흐름 제어(Flow Control)
대부분의 정점,픽셀 셰이더 하드웨어는 명령어를 한번씩만 수행하도록 선형적으로 디자인되어있다. 그러나, HLSL은 정적 분기(static branching), 프레디케이션 명령(predicated instructions), 정적 반복(static looping), 동적 분기(dynamic branching), 동적 반복(dynamic looping) 등의 흐름 제어를 지원한다.
전에는 HLSL에서 if문을 사용하면 어셈블리 셰이더 코드에서 if문의 참/거짓의 경우를 모두 수행하되, 한쪽의 결과값만 남도록 컴파일되었다.
다음 코드를 vs_1_1로 컴파일 해보자.
if (Value > 0)
Position = Value1;
else
Position = Value2;
다음 코드가 앞의 코드를 vs_1_1로 컴파일해서 만들어진 어셈블리 코드이다.
// 선형보간값 r0.w를 계산
mov r1.w, c2.x
slt r0.w, c3.x, r1.w
// value1과 value2사이를 선형보간
mov r7, -c1
add r2, r7, c0
mad oPos, r0.w, r2, c1
그러나, 어떤 하드웨어는 정적이나 동적 반복 중 하나만 지원하며, 선형적인 수행을 필요로 한다. 반복을 지원하지 않는 모델에서는 반복할 내용을 반복 회수만큼 전개(unroll)해서 컴파일 하게 된다. 이러한 경우의 좋은 예는 DirectX 9.0 SDK의 DepthOfField예제인데, 여기서는 ps_1_1에서도 작동하도록 전개(unroll)를 사용해서 컴파일 하고 있다.
HLSL은 이제 다음과 같은 형태의 흐름제어도 포함되어 있다.
• 정적 분기(static branching)
• 프레디케이션 명령(predicated instructions)
• 정적 반복(static looping)
• 동적 분기(dynamic branching)
• 동적 반복(dynamic looping)
요즘 하드웨어에서 지원하는 대부분의 분기지원은 정적 분기이다. 정적분기는 불린 셰이더 상수에 의해서 특정 코드블럭이 실행될지 안될지를 결정하는 것이다. 이것은 현재 렌더링 되려는 오브젝트의 형태에 따라서 코드를 허용하거나 금지시킬 때 편리한 방법이다. 화면에 그려질 때, 현재 셰이더의 특정 기능을 사용할지를 결정 할 수 있는 것이다. HLSL에서는 분기를 허용하기 때문에서 불린 상수에 의해서 금지된 코드는 실행되지 않고 건너뛰게 된다.
개발자에게 가장 친숙한 분기는 동적분기이다. 동적분기에서는 비교해야 할 상태가 변수에 들어있어서, 비교가 컴파일 시(compile time)에 일어나는 것이 아니라 실행 시(runtime)에 일어나게 된다. 수행능력은 "분기에 필요한 비용 + 분기에 의해서 실행된 명령에 따른 비용"으로 결정된다. 동적분기는 현재 정점셰이더에서 사용 가능하다.
5절 함수(Functions)
• 함수 선언(Function Declaration)
• 함수 반환 형(Function Return Types)
• 함수 명(Function Name)
• 전달인자 리스트(Argument Lists)
• 함수 몸체(Function Body)
• 사용자 정의 함수(User-Defined Functions)
함수는 큰 작업을 작은 작업들로 분할하며, 작은 작업은 디버깅과 재사용이 훨씬 용이하다. 또한, 함수는 함수자체의 자세한 내용을 숨기기 위해서도 사용되며, 프로그램은 이러한 함수들의 조합으로 이루어져있다.
HLSL함수는 몇가지 면에서 C의 함수와 비슷하다. 일단, 함수는 '함수정의'와 '함수몸체'를 포함하고 있어야 하며, '반환형'과 '전달인자 리스트'를 선언해야 한다. C함수와 마찬가지로 HLSL도 셰이더 컴파일시에 문법의 적법성을 검사하기 위해 전달인자, 전달인자 형, 반환값을 체크한다.
그러나, C함수와는 다르게, HLSL의 진입함수는 함수 전달인자와 셰이더 입출력값을 결합하기 위해서 의미자를 사용하는데, 결과적으로 '버퍼 데이터를 셰이더와 결합하는 것'과 '셰이더 출력값을 셰이더 입력값과 결합하는 것'을 쉽게해준다.(내부적으로 호출되는 HLSL함수는 의미자를 무시한다.)
5.1 함수 선언(Function Declaration)
함수는 선언부와 몸체로 이루어져 있는데, 선언부는 몸체보다 먼저 나와야 한다.
float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
return mul(inPos, WorldViewProj );
}
함수 선언부에는 {}앞에 모든 것이 나와있어야 한다.
float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
함수 선언부에는 다음과 같은 것들이 포함되어 있다.
• 반환 값
• 함수 명
• 전달인자 리스트(선택적)
• 출력 의미자(선택적)
• 설명문(annotation)(선택적)
5.2 함수 반환형(Function Return Types)
반환형은 float4처럼 HLSL의 어떠한 기본 자료형도 가능하다.
float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
...
}
반환형은 미리 정의된 자료구조도 가능하다.
struct VS_OUTPUT
{
float4 vPosition : POSITION;
float4 vDiffuse : COLOR;
};
VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
...
}
함수가 반환값이 없으면, 반환형을 void로 하면된다.
void VertexShader_Tutorial_1(float4 inPos : POSITION )
{
...
}
반환형은 항상 함수 선언의 앞부분에 나타나야 한다.
5.3 함수 명(Function Name)
함수명은 함수의 반환형 다음에 나타나는 식별자이다.
float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
함수명은 VertexShader_Tutorial_1처럼 이해하기 쉽게 짓는 것이 좋다.
5.4 전달인자 리스트(Argument Lists)
전달인자 리스트 함수에게 입력 인자와 반환될 값을 선언할 수 있다. 어떤 인자를 입력과 출력 모두에 사용될 수도 있다.
다음 예제는 4개의 입력 인자를 가진 셰이더이다.
float4 Light(float3 LightDir : TEXCOORD1,
uniform float4 LightColor,
float2 texcrd : TEXCOORD0,
uniform sampler samp) : COLOR
{
float3 Normal = tex2D(samp,texcrd);
return dot((Normal*2 - 1), LightDir)*LightColor;
}
이 함수는 샘플링된 텍스처와 조명색깔을 혼합한 최종 색깔을 반환한다. 이 함수는 4개의 입력값이 있고, 그 중 2개는 의미자가 있는데, LightDir는 TEXCOORD1, texcrd는 TEXCOORD0이다. 이것은 이 변수들이 정점버퍼로부터 입력 된다는 뜻이다.
LightDir가 TEXCOORD1의 의미자를 갖고 있지만, 이 전달인자는 아마도 텍스처 좌표가 아닐 것이다. TEXCOORDn의미자는 주로 미리 정의되어 있지 않은 자료형일 경우에 자주 사용되는데, 이 경우에도 정점셰이더 의미자에는 광원방향에 대한 것이 없으므로 TEXCOORD1을 사용한 것으로 이해할 수 있을 것이다.
다른 2개의 입력인 LightColor와 samp는 uniform명령어가 있다. 그 얘기는 화면에 그려지는 중에 이 변수값이 바꾸지 않는 다는 뜻이다. 대부분 이런 전달인자는 셰이더 전역 변수로부터 전달된다.
전달인자는 입력값의 경우 in명령어, 출력값일 경우 out명령어와 함께 사용할 수 있다. 전달인자는 참조에 의해서 전달될 수 없으나, inout명령어를 사용하면 입출력값으로 사용될 수 있다. inout명령어로 선언되어 함수로 전달되는 전달인자는 함수 반환전까지는 원본의 복사본으로 간주되다가, 되돌아 갈때 다시 복사된다.
inout명령어를 사용한 예를 보도록 하자.
void Increment_ByVal(inout float A, inout float B)
{
A++; B++;
}
이 함수는 A와 B값을 증가시키고, 그 값을 반환한다.
5.5 함수 몸체(Function Body)
함수 몸체란 함수 선언다음에 나오는 모든 코드를 말하는 것으로, {}으로 둘러싸인 문장으로 구성되며, 변수, 리터럴, 식, 문장을 사용해서 구현한다.
float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
return mul(inPos, WorldViewProj );
}
이 셰이더 몸체는 2가지 일을 하는데, 행렬곱셈과 float4를 반환하는 것이다. 여기서, 행렬곱셈은 mul함수를 사용해서 이루어진다. mul은 HLSL라이브러리 함수에 이미 구현되어져 있기때문에 내부함수(intrinsic function)라 부른다.
이 코드의 행렬 곱셈은 입력벡터 Pos와 합성행렬인 WorldViewProj의 조합으로 이루어져있다. 결과적으로 위치값은 화면좌표계로 변환된다. 이것이 우리가 할수 있는 최소한의 정점셰이더 처리인 것이다. 만약 정점셰이더가 아닌 고정함수 파이프라인을 사용한다면 이 변환뒤에 정점데이터가 그려질 것이다.
함수몸체의 제일 마지막 문장은 반환문으로, C와 마찬가지로 return문은 제어권을 이 함수를 호출한 곳으로 되돌려 준다.
5.6 반환 형(Return Types)
함수 반환형은 HLSL에 정의된 bool, int, half, float, double등의 어떠한 자료형도 가능하다. 또한 복합 자료형인 vector, matrix도 가능하지만, pixelshader, vertexshader, texture, and sampler같은 객체형은 반환형이 될 수 없다.
구조체를 반환하는 자료형의 예를 보도록 하자.
float4x4 WorldViewProj : WORLDVIEWPROJ;
struct VS_OUTPUT
{
float4 Pos : POSITION;
};
VS_OUTPUT VS_HLL_Example(float4 inPos : POSITION )
{
VS_OUTPUT Out;
Out.Pos = mul(inPos, WorldViewProj );
return Out;
}
5.7 사용자 정의 함수(User-Defined Functions)
함수 정의는 C와 매우 비슷하다.
[static inline target] [const] type id ( [parameter_list] ) ;
[static inline target] [const] type id ( [parameter_list] ) { [statements] } ;
타겟이란 선택적인 식별자로 함수가 사용될 플랫폼을 지정한다.
parameter_list는 하나 이상의 전달인자 선언들을 콤마(,)로 구분하여 나열한 것이다.
[uniform in out inout] type id [: semantic] [= default]
전달인자 값은 항상 값에 의해서 전달된다.
• in명령어가 사용된 전달인자는 사용하기 전에 먼저 복사되어야 한다는 것이다.
• out명령어가 사용된 전달인자는 전달인자의 최종값이 복사되어, 함수를 호출한 곳으로 전달되어야 한다는 것이다.
• inout명령어가 사용된 전달인자는 in과 out을 동시에 지정하는 것이다.
• uniform명령어가 사용된 전달인자는 in명령어의 특별한 형태로, 최상위 함수일 경우 상수데이터가 전달인자로 전달된다는 것이다.
• 최상위 함수가 아닐 경우에는 in과 동일한 역할을 한다.
• in,out,inout,uniform중 어떤것도 지정되지 않으면 in으로 간주한다.
함수몸체가 없는 함수는 함수형태만 선언한 것으로 간주되며, 뒤에 몸체와 함께 반드시 재정의 되어야 한다. 만약 함수 몸체가 정의되지 않은 함수가 참조될 경우에는 오류가 발생한다.
함수는 다중정의(overloading)될 수 있으며, 함수명, 전달인자 형, 타겟 플랫폼에 의해서 식별될 수 있다. 그러나, 함수 다중정의는 아직 구현되어 있지 않다.
현재 모든 함수는 인라인(inline)이므로, 되부름은 지원되지 않는다.
6절 내부함수(Intrinsic functions)
내부함수란 HLSL컴파일러에서 지원하는 라이브러리 함수이다. 이미 HLSL내부에 구현되어 있는 함수들이며, 상당한 최적화가 이루어져 있으므로, 내부함수를 많이 사용할수록 효율적인 코드가 만들어진다.
함수명 구문 설명
abs value abs(value a) 절대값 (성분마다).
acos acos(x) x 의 각 성분의 역코사인을 돌려준다. 각 성분은,[-1, 1] 의 범위로 한다.
all all(x) x 의 모든 성분이 0 이외의 값인지 아닌지를 테스트한다.
any any(x) x 의 몇개의 성분이 0 이외의 값인지 아닌지를 테스트한다.
asin asin(x) x 의 각 성분의 역사인을 돌려준다. 각 성분은,[-pi/2, pi/2] 의 범위로 한다.
atan atan(x) x 의 각 성분의 역탄젠트를 돌려준다. 반환값은,[-pi/2, pi/2] 의 범위이다.
atan2 atan2(y, x) y/x 의 역탄젠트를 돌려준다. y 와 x 의 부호를 사용해 [-pi, pi] 의 범위에 있는 반환
값의 상한을 판단한다. atan2 는, x 가 0 으로 동일하고, y 가 0 으로 동일하지 않은 경우에서도, 원점 이
외의 각 점에 대해서 잘 정의(well-defined)되어 있다.
ceil ceil(x) x 이상의 최소의 정수를 돌려준다.
clamp clamp(x, min, max) x 를 [min, max] 의 범위로 제한한다.
clip clip(x) x 의 어떤 성분이 0 보다 작은 경우, 현재의 픽셀을 파기한다. x 의 각 성분이 면으로부터의
거리를 나타내는 경우, 이 함수를 사용해, 클립면을 시뮬레이션 한다.
cos cos(x) x 의 코사인을 돌려준다.
cosh cosh(x) x 의 쌍곡코사인(hyperbolic cosine)을 돌려준다.
cross cross(a, b) 2 개의 3D 벡터 a 와 b 의 외적을 돌려준다.
D3DCOLORtoUBYTE4 D3DCOLORtoUBYTE4(x) 4D 벡터 x 의 성분을 교체 및 스케일링 해, 일부
하드웨어가 지원하지 않는UBYTE4를 지원한다.
ddx ddx(x) 스크린 공간의 x 좌표에 대해, x 의 편미분을 돌려준다.
ddy ddy(x) 스크린 공간의 y 좌표에 대해, x 의 편미분을 돌려준다.
degrees degrees(x) x 를 라디안 단위로부터 도(degree)로 변환한다.
determinant determinant(m) 정방 행렬 m 의 행렬식을 돌려준다.
distance distance(a, b) 2 개의 점 a 와 b 간의 거리를 돌려준다.
dot dot(a, b) 2 개의 벡터 a 와 b 의 내적을 돌려준다.
exp exp(x) e 를 밑으로 하는 지수
exp2 value exp2(value a) 2 를 밑으로 하는 지수
faceforward faceforward(n, i, ng) -n * sign(dot(i, ng))를 돌려준다.
floor floor(x) x 이하의 최대의 정수를 돌려준다.
fmod fmod(a, b) a / b 의 부동 소수 나머지(잉여) f를 돌려준다.
단, i는 정수, f 는 x 와 부호가 같고, f의 절대값은 b 의 절대값보다 작아야 한다.
frac frac(x) x의 소수부 f를 돌려준다.
frc value frc(value a) 소수부 (성분마다).
frexp frexp(x, out exp) x 의 가수와 지수를 돌려준다. frexp 는 가수를 돌려주며, 지수는 출력 인수 exp에 저장 된다. x 가 0 의 경우, 함수는 가수와 지수의 양쪽 모두에 0 을 돌려준다.
fwidth fwidth(x) abs(ddx(x)) +abs(ddy(x))를 돌려준다.
isfinite isfinite(x) x가 유한의 경우는 TRUE 를 돌려준다. 그 이외의 경우는 FALSE를 돌려준다.
isinf isinf(x) x가 +INF나 -INF 의 경우는 TRUE를 돌려준다. 그 이외의 경우는 FALSE를 돌려준다.
isnan isnan(x) x가 NAN 이나 QNAN의 경우는 TRUE를 돌려준다. 그 이외의 경우는 FALSE를 돌려준다.
ldexp ldexp(x, exp) x * 2exp를 돌려준다.
len float len(value a) 벡터의 길이.
length length(v) 벡터 v 의 길이를 돌려준다.
lerp lerp(a, b, s) a + s(b - a)를 돌려준다. 이 함수는, s가 0의 경우는 a, 1의 경우는 b를 돌려주도록, a와 b사이를 선형 보간 한다.
lit lit(ndotl, ndoth, m) 조명의 벡터 (주변, 확산, 반사, 1)를 돌려준다.
주변 = 1;
확산 = (ndotl < 0) ? 0 : ndotl;
반사 = (ndotl < 0) || (ndoth < 0) ? 0 : (ndoth * m);
log log(x) 밑이 e인 x 의 자연대수를 돌려준다. x가 음수(-)일 경우, 무한을 돌려준다. x 가 0일 경우, +INF를 돌려준다.
log10 log10(x) 밑이 10인 x 의 자연대수를 돌려준다. x가 음수(-)일 경우, 무한을 돌려준다. x 가 0일 경우, +INF를 돌려준다.
log2 log2(x) 밑이 2인 x 의 자연대수를 돌려준다. x가 음수(-)일 경우, 무한을 돌려준다. x 가 0일 경우, +INF를 돌려준다.
max max(a, b) a와 b중 큰 쪽을 선택한다.
min min(a, b) a와 b중 작은쪽을 선택한다.
modf modf(x, out ip) 값x를 소수부와 정수부로 나눈다. 부호있는 소수부는 반환값으로 돌려주고, 부호있는 정수부는 출력 인수 ip로 돌려준다.
mul mul(a, b) a와 b 사이의 행렬 곱셈을 실행한다. a가 벡터일 행벡터로, b가 벡터의 경우, 열벡터로 처리한다. a행과 b열의 크기는 동일해야 하며, 연산의 결과는 a행x b열 크기의 차원을 갖는다.
noise noise(x) 구현되어 있지 않음
normalize normalize(v) 정규화된 벡터 v / length(v)를 돌려준다. v 의 길이가 0 의 경우, 결과는 무한이 된다.
pow pow(x, y) xy 를 돌려준다.
radians radians(x) x를 도에서 라디안 단위로 변환한다.
reflect reflect(i, n) 입사 방향 i, 표면 법선 n으로 했을 경우의, v = i - 2 * dot(i, n) * n 에 의해 구할 수 있는, 반사 벡터 v 를 돌려준다.
refract refract(i, n, eta) 입사 방향 i, 표면 법선 n, 굴절 eta인 상대 인덱스가 주어졌을 경우의, 굴절 벡터 v 를 돌려준다. i 와 n 의 사이의 입사각이 지정된 eta 보다 너무 크면 (0,0,0)을 돌려준다.
round round(x) x를 가장 가까운 정수에 반올림한다.
rsqrt rsqrt(x) 1 / sqrt(x)를 돌려준다.
saturate saturate(x) x를 [0, 1] 의 범위에 제한한다.
sign sign(x) x의 부호를 얻는다. x 가 0보다 작은 경우는 -1, 0과 동일한 경우는 0, 0 보다 큰 경우는 1을 돌려준다.
sin sin(x) x의 사인을 돌려준다.
sincos sincos(x, out s, out c) x의 사인과 코사인을 돌려준다. sin(x)는 출력 인수 s에 저장 되며 cos(x)는 출력 인수 c에 저장 된다.
sinh sinh(x) x 의 쌍곡사인을 돌려준다.
smoothstep smoothstep(min, max, x) x < min 의 경우는 0을 돌려준다. x > max 의 경우는 1을 돌려준다. x 가 [min, max] 의 범위내이면, 0부터 1 의 사이의 매끄러운 에르미트 보간을 돌려준다.
sqrt value sqrt(value a) 제곱근 (성분마다).
step step(a, x) (x >= a) ? 1 : 0 을 돌려준다.
tan tan(x) x 의 탄젠트를 돌려준다.
tanh tanh(x) x 의 쌍곡탄젠트를 돌려준다.
tex1D tex1D(s, t) 1D 의 텍스처 참조. s 는 샘플러 또는 sampler1D 객체. T는 스칼라
tex1D tex1D(s, t, ddx, ddy) 도함수를 지정한, 1D 의 텍스처 참조. s 는 샘플러 또는 sampler1D 개체.
t, ddx, ddy 는 스칼라
tex1Dproj tex1Dproj(s, t) 1D의 투영 텍스처 참조. s 는 샘플러 또는 sampler1D 개체. t 는 4D 벡터. t 는, 참조가 실행되기 직전의 성분으로 나눗셈 된다.
tex1Dbias tex1Dbias(s, t) 1D 의 바이어스 텍스처 참조. s 는 샘플러 또는 sampler1D 개체. t 는 4D 벡터. 참조를 실행하기 전에, 밉레벨이 t.w에 의해 바이어스 된다.
tex2D tex2D(s, t) 2D 의 텍스처 참조. s 는 샘플러 또는 sampler2D 개체. t 는 2D 텍스처 좌표.
tex2D tex2D(s, t, ddx, ddy) 도함수를 지정한, 2D 의 텍스처 참조. s 는 샘플러 또는 sampler2D 개체. t, ddx, ddy 는 2D 벡터.
tex2Dproj tex2Dproj(s, t) 2D 의 투영 텍스처 참조. s 는 샘플러 또는 sampler2D 개체. t 는 4D 벡터. t 는, 참조가 실행되기 직전의 성분으로 나눗셈 된다.
tex2Dbias tex2Dbias(s, t) 2D 의 바이어스 텍스처 참조. s 는 샘플러 또는 sampler2D 개체. t 는 4D 벡터. 참조를 실행하기 전에, 밉레벨이 t.w에 의해 바이어스 된다.
tex3D tex3D(s, t) 3D 의 볼륨 텍스처 참조. s 는 샘플러 또는 sampler3D 개체. t 는 3D 텍스처 좌표.
tex3D tex3D(s, t, ddx, ddy) 도함수를 지정한, 3D 의 볼륨 텍스처 참조. s 는 샘플러 또는 sampler3D 개체. t, ddx, ddy 는 3D 벡터.
tex3Dproj tex3Dproj(s, t) 3D 의 투영 볼륨 텍스처 참조. s 는 샘플러 또는 sampler3D 개체. t 는 4D 벡터. t 는, 참조가 실행되기 직전의 성분으로 나눗셈 된다.
tex3Dbias tex3Dbias(s, t) 3D 의 바이어스 텍스처 참조. s 는 샘플러 또는 sampler3D 개체. t 는 4D 벡터. 참조를 실행하기 전에, 밉레벨이 t.w에 의해 바이어스 된다.
texCUBE texCUBE(s, t) 3D 의 큐브 텍스처 참조. s 는 샘플러 또는 samplerCUBE 개체. t 는 3D 텍스처 좌표.
texCUBE texCUBE(s, t, ddx, ddy) 도함수를 지정한, 3D 의 큐브 텍스처 참조. s 는 샘플러 또는 samplerCUBE 개체. t, ddx, ddy 는 3D 벡터.
texCUBEproj texCUBEproj(s, t) 3D 투영의 큐브 텍스처 참조. s 는 샘플러 또는 samplerCUBE 개체. t 는 4D 벡터. t 는, 참조가 실행되기 직전의 성분으로 나눗셈 된다.
texCUBEbias texCUBEbias(s, t) 3D 의 바이어스 큐브 텍스처 참조. s 는 샘플러 또는 samplerCUBE 개체. t 는 4D 벡터. 참조를 실행하기 전에, 밉레벨이 t.w에 의해 바이어스 된다.
transpose transpose(m) 행렬 m 의 전치행렬을 돌려준다. 입력의 크기가 m 행 x n 열의 경우, 결과차원은 n열x m행이다.