Graphics Programming
픽셀 벤더 개발자 안내서 [2/2] 본문
글이 길어서 두 개로 나눴습니다
서문
1 픽셀 벤더 툴킷 개요
픽셀 벤더 툴킷 설치하기
픽셀 벤더 툴킷 패키지의 내용물
시작하기
예제들
픽셀 벤더 개념들
파일 포맷
커널 프로그램 부분
내장 함수
픽셀 벤더 필터와 이펙트 사용하기
포토샵용으로 개발하기
애프터 이펙트용으로 개발하기
플래시용으로 개발하기
2 시작하기
픽셀 벤더 툴킷 IDE에 익숙해지기
새 픽셀 벤더 커널 프로그램 제작하기
커널 프로그램 편집하고 실행하기
그림 채널 분리하기
필터 연산을 감마로 바꾸기
매개변수 추가하기
매개변수 통제하기
최종 소스
3 픽셀 벤더 필터 작성하기
커널의 부분들
커널 메타데이터
커널 멤버
매개변수와 변수
픽셀 벤더 좌표계
픽셀 좌표에 접근하기
좌표 넘기기
입력 그림 그리고 추출
정사각형이 아닌 픽셀
여러 입력 그림
종속 값 사용하기
보조 함수
영역 함수 예제
보조 함수 예제
4 영역으로 작업하기
픽셀 벤더가 커널을 실행하는 방법
그래프에서 영역 추론하기
영역 결정하기
경계에서 하는 계산을 위해 그림 크기 조절하기
안전한 영역 경계
필요역 계산하기
needed() 함수 안에서 커널 매개변수에 접근하기
DOD로 필요역 계산하기
변경역 계산하기
한층 복잡한 예제
생성역 계산하기
5 픽셀 벤더 그래프 언어
그래프 요소들
간단한 그래프 예제
진짜 간단한 예제
복잡한 그래프
그래프 정의하기
그래프 매개변수와 커널 매개변수 정의하기
여러 마디 정의하기
복잡한 연결 정의하기
진짜 복잡한 예제
그래프 편집기 사용하기
6 애프터 이펙트용으로 개발하기
애프터 이펙트 커널 메타데이터
채널 넷인 값에 접근하기
회선 예제
애프터 이펙트에서의 커널 매개변수
회선 커널 확장
종속 함수
7 플래시용으로 개발하기
픽셀 벤더 필터를 SWF에 끼워넣기
픽셀 벤더 필터를 액션스크립트 라이브러리로 만들기
필터를 액션스크립트 클래스로 감싸기
SWC 라이브러리 만들기
4 영역으로 작업하기
픽셀 벤더 런타임 엔진은 각 출력 픽셀에 대해 커널을 실행하여 출력 그림을 만들어냅니다. 커널 여러 개를 묶은 것인 그래프의 경우 한 커널이 내놓는 출력 픽셀은 다음 커널에 대한 입력 픽셀이 됩니다. 이 과정을 효율적으로 하기 위해 그래프는 픽셀 벤더 런타임 엔진이 최종 표시에 영향을 끼치지 않는 픽셀을 계산에서 제외시킬 수 있게 도웁니다.
GaussianBlur 커널 같은 앞서 나온 예제들에서 커널이 어떻게 지금 픽셀 외의 픽셀에 접근하는지 보았습니다. 출력 그림 속 픽셀 하나의 값은 입력 그림의 여러 픽셀에 의존할 수 있습니다. 즉 출력 그림의 픽셀 하나는 입력 그림의 픽셀 영역에 의존합니다. 출력 그림을 최대한 효율적으로 계산해내려면 입력 그림의 어떤 픽셀들이 필요한지 픽셀 벤더 런타임 엔진에 정확히 알려야 합니다. 비슷하게 입력 그림 속 픽셀 하나가 출력 그림의 여러 픽셀에 영향을 끼칠 수 있습니다. 입력 그림의 픽셀 하나가 변하면 여러 출력 픽셀을 다시 계산해야 할 수도 있습니다.
커널이 지금 픽셀이 아닌 픽셀에 접근한다면 커널에 영역 함수들을 구현하여, 어느 영역을 계산에 넣어야 하는가에 대한 뚜렷한 정보를 픽셀 벤더 런타임 엔진에 줘야 합니다.
플래시 주석: 플래시 플레이어에서 픽셀 벤더를 쓸 때는 영역 함수들을 사용할 수 없습니다.
픽셀 벤더는 어떻게 커널을 실행할까
사용자가 픽셀 벤더 커널 프로그램에 정의된 필터를 발동! 하면 픽셀 벤더 런타임 엔진은 이 과정을 밟습니다.
1. 종속 값들을 구하고
2. 영역 함수들이 있으면 써먹어서 입력 영역과 출력 영역을 결정하고
3. 출력 영역의 각 픽셀에 대해 evaluatePixel() 함수를 실행하여 출력 그림을 만듭니다.
엔진은 먼저 영역을 정의하여 불필요한 픽셀들을 계산에서 빼어 그림 처리 효율을 확 끌어올립니다. 엔진은 커널 정의 속에 알게 모르게 들어 있는 영역 정보를 활용합니다.
▶ 정의역(domain of definition, DOD): 고려할 픽셀들이 모두 든 입력 영역. "무얼 가지고 시작해야 하지?"로 여기세요.
모든 그림에는 자기를 정의하는 영역, 즉 DOD가 있습니다. 예를 들어 너비는 200 픽셀이고 폭은 100 픽셀인 PNG 파일을 하드 드라이브에서 읽으면 정의역은 (0, 0, 200, 100)입니다.
▶ 관심역(region of interest, ROI): 계산해서 출력해야 하는 픽셀 영역. "무얼 다뤄야 하지?"로 여기세요.
커널은 영역 함수들을 정의하여("영역 결정하기"를 보세요) ROI 정보를 제공함으로써 다음 질문들에 답합니다.
▷ 이 영역을 출력하고 싶은데 입력의 어느 영역이 계산에 필요한가요?
▷ 입력의 이 영역을 고쳤는데 이것 때문에 출력의 어느 영역이 바뀌나요?
▷ 모든 입력 그림이 크기가 없어도 만들어내야 하는 출력 영역이 어딘가요?
그래프에서 영역 추론하기
한 커널에서는 입력 그림인 게 다른 커널에서는 출력 그림인, 그래프에서 영역 결정이 특히 중요합니다.
그래프 정의에는 그래프 전체에 적용되는 영역 함수가 없지만 그래프 속 커널들에는 보통 영역 함수가 있습니다. 픽셀 벤더 런타임 엔진은 어떤 커널 작업이든 하기 전에, 마지막 마디에서 필요한 출력 영역을 결정하고 마디 사슬을 거꾸로 밟고 나아가 각 마디의 필요역을 확인해 그보다 앞선 노드의 출력 영역을 결정합니다. 이렇게 하여 엔진은 최종 출력에 쓰이지 않는 모든 픽셀을 계산에서 뺍니다.
예를 들어보지요. 커널 둘로 된 짧은 사슬에서 GaussianBlur 커널이 두 번째라고 칩시다.
그림이 이 그래프를 흐를 때, 각 중간 그림에는 마지막 출력 그림이 그렇듯 DOD가 있습니다.
그래프의 마지막 출력이 최초 ROI이며 엔진은 그래프를 거꾸로 추적해 각 중간 그림의 ROI를 결정합니다. 이 경우 픽셀 벤더 런타임 엔진은 GaussianBlur 커널이 내놓는 최종 출력을 정확히 계산하기 위해 Sharpen 커널이 출력해야 하는 영역을 계산합니다.
그래프는 5장 "픽셀 벤더 그래프 언어"에서 자세히 다룹니다.
더 읽을거리: 영역 계산을 더 자세히 알고 싶으시면 Michael A. Shantzis의 '효율적이고 유연한 그림 처리를 위한 모델'(http://portal.acm.org/citation.cfm?id=192191)을 보세요.
영역 결정하기
커널에 다음 영역 함수들을 정의할 수 있으며 각 영역 함수는 불분명한 region 자료형을 반환합니다. 픽셀 벤더 런타임 엔진이 커널 작업의 입력 그림과 출력 그림을 위해 공간을 얼마나 할당해야 하는가를 이 함수들로 알립니다.
▶ needed() 함수는 출력하고픈 영역을 만들기 위해 입력 그림에서 계산에 넣어야 하는 픽셀 영역(필요역)을 지정합니다. 이 함수는 출력 그림의 ROI로 입력 그림(들)의 ROI를 결정합니다.
▶ changed() 함수는 입력 픽셀들이 바뀔 경우 출력 그림의 어느 영역을 다시 계산해야 하는가를 명기합니다. 보통 needed() 함수와 같거나 반대입니다. 이 함수는 입력 그림(들)의 DOD로 출력 그림의 DOD를 결정합니다.
▶ generated() 함수는 입력 그림이 전무해도 유효한 픽셀이 만들어질 출력 영역을 명기합니다. 이는 전적으로 커널에 의해 만들어진 출력 그림의 DOD를 결정합니다.
커널을 실행하면 픽셀 벤더 런타임 엔진은 각 입력 그림에서 evaluatePixel()을 호출하기에 앞서 needed() 함수와 changed() 함수를 호출합니다.
영역 함수들은 선택 사항입니다. 영역 함수가 하나도 없으면 픽셀 벤더 런타임 엔진은 커널이 점별(pointwise), 즉 각 입력에서 지금 출력 픽셀과 위치가 같은 입력 픽셀에만 접근한다고 가정합니다. 점별 커널에서는 요구 출력보다 입력들이 크기만 하면 됩니다. (요구 출력 크기는 커널 작성자가 아니라 애플리케이션이 결정하는 무한 그림 평면의 부분입니다. 이 출력 크기는 정의역 전체─"그림 전체"─나 홑 타일(single tile)이나 다른 하위 영역에 상응할 것입니다.)
하지만 점별 작업에서조차 작업 중 가려진다거나 하는 픽셀은 영역 함수로 제외시켜 효율성을 높일 수 있습니다. MatteRGBA가 그런 용례입니다.
지금 픽셀의 양 옆 픽셀에 접근하는 HorizontalAverage 커널이 그렇듯 많은 유용한 그림 처리 작업이 점별이 아닙니다. 입력 그림마다 픽셀을 여러 개 뽑아 자료를 변형하여 회선(convolution) 또는 수집(gathering) 작업을 하는 커널들이 비점별입니다. 이런 커널들은 작업에 필요한 영역, 작업에 영향을 미치는 영역을 정확히 식별해야 결과가 정확하고 작업이 효율적입니다.
애프터 이펙트 주석: 애프터 이펙트용 점별 커널에는 needed() 함수와 changed() 함수를 정의해야 합니다. 그러지 않으면 작업이 비효율적이어서 그림이 올바르게 만들어지지 않을 것입니다.
경계에서 하는 계산을 위해 그림 크기 조절하기
비점별 작업에서는 주변 픽셀 값들이 각 출력 픽셀 계산에 영향을 줍니다. 이 간단한 흐림 커널은 각 픽셀을 양 옆 이웃과 평균냅니다.
kernel HorizontalAverage
<...>
{
input image4 source;
output pixel4 result;
void evaluatePixel()
{
float2 coord = outCoord();
float2 hOffset = float2(pixelSize(source).x, 0.0);
pixel4 left = sampleNearest(source, coord - hOffset);
pixel4 center= sampleNearest(source, coord);
pixel4 right = sampleNearest(source, coord + hOffset);
result = (left + center + right) / 3.0;
}
}
이 커널이 결과를 정확히 내려면 각 픽셀의 양 옆 이웃에 접근해야 합니다. 하지만 그림의 오른쪽 끝에서는 오른쪽 이웃에 픽셀 자료가 없고 왼쪽 끝에서는 왼쪽 이웃에 자료가 없습니다. ROI에 필요한 자료가 입력 그림의 DOD에 모두 있는 게 아니라는 것이지요.
픽셀 벤더는 그림의 정의역 밖 픽셀의 경우 추출 함수가 투명한 검은빛을 쓰도록 합니다. 마치 그림에 끝없이 뻗어나가는 투명하고 검은 경계가 있는 것처럼요. 이 평균 필터의 경우 그림의 양 끝 출력 픽셀 값을 계산하면 픽셀이 더 투명하게 되어버리는데, 투명한 검정과 평균을 내기 때문입니다.
그림 크기를 고수할 생각이라면 출력에서 가장자리가 투명해지는 걸 받아들이거나 가장자리는 따로 모종의 처리를 해야 합니다. 투명도가 올라가길 바라지 않지만 경계를 특수 처리하고 싶지도 않다면 출력 그림의 크기를 입력 그림보다 줄여야 합니다.
예를 들어 입력 그림이 너비 10 픽셀, 폭 9 픽셀이면 출력 그림은 투명도를 유지하기에는 너비 8 픽셀, 폭 9 픽셀이 최대입니다. 이렇게 하면 양 가장자리의 자료를 정확히 추출합니다.
거꾸로 8 픽셀 x 9 픽셀 출력 그림이 필요하면 입력 영역이 적어도 10 x 9는 되라야 추출이 정확하겠지요. 이게 필요역(needed region)이고 needed() 함수로 지정할 수 있습니다. (조금 밑으로 내리면 나오는 예제를 보세요)
입력 그림이 필요역보다 작으면 어찌할 방도가 없습니다. 추출 함수는 기본값인 투명 검정을 쓰고 픽셀은 투명도가 올라갑니다. 하지만 그래프 안에서 입력 그림이 앞선 커널로부터 오는 거라면 다음 커널에 필요역을 지정해 픽셀 벤더 런타임 엔진에게 앞선 커널이 그림을 충분히 크게 만들어낸다고 확신시킬 수 있습니다.
안전한(conservative) 영역 경계
영역 함수들이 언제나 정확하고 픽셀에 최적인 경계를 반환한다고 기대해선 안 됩니다. 경계가 모호하거나 정확히 계산하기 어려운 일이 흔히 일어납니다. 반환된 영역은 차라리 안전 경계(conservative bounds)여야 합니다. 이 경계는 이상적인 최소 영역을 완전히 포함해야 합니다. 경계를 너무 크게 만들면 저장 공간을 낭비하고 계산을 더 합니다. 경계를 심히 과장하면 작업이 매우 비효율적이지만, 경계를 너무 작게 만들면 더 나쁩니다. 아예 계산이 틀리게 되지요.
▶ needed() 영역이 너무 작으면 필요한 픽셀들이 공급되지 않아서 커널은 입력 그림(들)의 일부분을 투명한 검정으로 잘못 봅니다.
▶ changed() 영역이 너무 작으면 DOD에 그림 자료가 일부 포함되지 않아 그림이 잘리고, 틀린 캐시들이 옳은 것으로 저장되어버립니다.
▶ generated() 영역이 너무 작으면 DOD에 그림 자료가 일부 포함되지 않아 그림이 잘립니다.
결과의 진짜 경계가 그림 자료에 따라 다른 경우도 있습니다. 그런 경우에도 경계는 안전해야 합니다. 이에 대해 흔히 드는 예가 입력 맵의 픽셀 값에 따라 그림을 변형하는 위치바꿈맵(displacement map) 효과입니다. 픽셀 벤더에서 이 효과를 표현하려면 사용자가 제어할 수 있는 매개변수에 준하여 최대 이동 한계를 설정한다던가 해서 경계 계산 때 안전 반경으로 써야 합니다.
필요역 계산하기
needed() 함수를 정의하여 입력 그림의 ROI를 결정할 수 있습니다. 이로써 픽셀 벤더 런타임 엔진에게 해당 출력 영역을 계산하는 데 입력 픽셀이 얼마나 필요한지 알립니다. 특히 needed() 함수는 출력 영역의 픽셀을 모두 계산하기 위해 각 입력에서 접근해야 하는 픽셀들을 결정합니다. 그리고 픽셀 벤더 런타임 엔진은 이 픽셀들이 다 평가되며 대응하는 그림 객체를 통해 이 픽셀들에 접근할 수 있다고 확신합니다.
needed() 함수는 항상 region을 반환하고 인자를 두 개 받습니다.
▶ outputRegion ─ 요구 영역이나 필요 영역이라고도 하는 출력 영역은 출력 그림의 무한 평면 중 계산을 할 일부입니다. 즉, 평면에서 커널 함수를 호출해 픽셀 값들을 평가할 일부입니다. 출력 영역을 계산할 출력 그림의 크기로 여기세요.
▶ inputIndex ─ 커널은 입력을 얼마든지 받을 수 있으며 한 출력 영역에 대한 계산을 위해 입력마다 서로 다른 필요역에 접근할 수 있습니다. 예를 들어보겠습니다. 커널은 각 입력을 따로따로 변형시킬 수 있습니다. 그래서 needed() 함수는 ROI를 계산할 특정 입력에 대한 참조를 받습니다. 입력이 하나뿐인 커널의 경우 참조는 항상 그 그림에 대한 것이니 무시할 수 있습니다.
영역을 보통 산술 표현식에 넣을 수는 없고, 픽셀 벤더 커널 언어에는 영역을 다루는 커스텀 함수들이 뭉텅이입니다. 밑에서는 출력 영역을 받아 양 옆으로 한 픽셀씩 늘려 경계에서 추출하기에 충분한 자료를 포함하는 입력 영역을 찾도록 needed()를 정의합니다. 내장 영역 조작 함수들 중 하나인 outset()을 써서 영역 계산을 수행합니다. 또한 픽셀이 네모나지 않을 경우를 대비해 그림의 픽셀 크기를 고려합니다.
이 예제에서는 커널에 입력이 하나 뿐인지라 입력 인자를 무시합니다. inputIndex 값으로 여러 그림을 구별하는 예제를 원하시면 "더 복잡한 예제"에서 보세요.
kernel HorizontalAverage
<...>
{
input image4 source;
output pixel4 result;
region needed(region outputRegion, imageRef inputIndex) {
region result = outputRegion;
// region outset( region a, float2 amount ) 영역 a의 각 경계마다 amount만큼 늘립니다.
result = outset( result, float2(pixelSize(source).x, 0) );
return result;
}
void evaluatePixel()
{
float2 coord = outCoord();
float2 hOffset = float2(pixelSize(source).x, 0.0);
pixel4 left = sampleNearest(source, coord - hOffset);
pixel4 center= sampleNearest(source, coord);
pixel4 right = sampleNearest(source, coord + hOffset);
result = (left + center + right) / 3.0;
}
}
needed() 함수 안에서 커널 매개변수에 접근하기
영역 함수는 evaluateDependents()와 evaluatePixels()에서 할 수 있듯이 커널 매개변수와 종속 값을 참조해 이것들에 접근할 수 있습니다. 가변 크기 회선 같이 커널 매개변수에 따라 ROI가 달라질 경우 이게 유용해집니다.
kernel VariableHorizontalAverage
<...>
{
parameter int radius; // 단위는 픽셀
input image4 source;
output pixel4 result;
region needed(region outputRegion, imageRef inputIndex)
{
region result = outputRegion;
result = outset( result, float2( float( radius ) * pixelSize( source ).x, 0 ) );
return result;
}
void evaluatePixel()
{
result = pixel4(0,0,0,0);
float2 hOffset = float2(pixelSize(source).x, 0.0);
for (int i=-radius; i<=radius; i++)
result += sampleNearest( source, outCoord()+ float(i) * hOffset );
result /= float( radius*2 + 1 );
}
}
DOD로 필요역 계산하기
각 입력의 정의역을 영역 계산 도중 이용할 수 있습니다. needed() 함수는 어느 영역이든 반환할 수 있지만 DOD 속 픽셀들만 실제로 계산됩니다. 그러니까, ROI는 항상 DOD에 맞춰 잘립니다.
DOD를 알면 특정 최적화를 할 수 있습니다. 커널이 마스크를 이용해 한 소스를 뿌옇게 한다면 소스에서는 마스크 DOD에 가리는 부분만 필요하고, 또 마스크에서는 소스와 겹치는 부분만 필요합니다. 이 정보를 needed() 함수 안에서 활용하여 MatteRGBA 커널을 최적화할 수 있습니다. MatteRGBA가 점별 작업, 즉 각 입력에서 지금 입력 픽셀에만 접근하니까 needed() 함수가 연산을 정확히 하는 데 필수는 아닙니다만, needed() 함수를 추가하면 런타임 시스템이 각 입력에서 어차피 감춰질 잉여 픽셀들을 제거하기 때문에 효율성이 올라갑니다.
kernel MatteRGBA
<...>
{
input image4 source;
input image1 matte;
output pixel4 result;
region needed(region outputRegion, imageRef inputIndex) {
region result = outputRegion;
// source에 맞춰 자르고
// region intersect( region a, region b ) a와 b의 교차 영역을 반환합니다.
result = intersect( result, dod( source ) );
// mask에 맞춰 자릅니다
result = intersect( result, dod( matte ) );
return result;
}
void evaluatePixel() {
pixel4 in_pixel = sampleNearest( source, outCoord() );
pixel1 matte_value = sampleNearest( matte, outCoord() );
result = in_pixel * matte_value;
}
}
내장 함수 dod()는 image나 imageRef를 받아 그 그림의 정의역을 전역 좌표로 반환합니다.
변경역 계산하기
changed() 함수는 needed() 함수의 반대로 생각하세요. 이 함수는 기입한 입력 영역의 픽셀이 변하면 영향을 받을 픽셀들을 계산합니다. 이건 캐시 무효화(cache invalidation) 같은 것의 중요 요인입니다. 이 함수는 그림의 DOD를 계산하는 데도 쓰입니다.
보통 changed() 함수의 몸체는 needed() 함수와 비슷하거나 심지어 같기까지 합니다. 여기VariableHorizontalAverage에서의 정의처럼요.
region changed( region inputRegion, imageRef inputIndex)
{
region result = inputRegion;
result = outset( result, float2( float( radius ) * pixelSize( source ).x, 0 ) );
return result;
}
항상 이렇지는 않습니다. 보통 비대칭 커널은 needed()와 changed()가 서로 반대입니다. 그림에 대해 기하 변형을 수행하는 커널은 두 함수의 변형 방법을 반대로 해야 합니다.
changed() 함수가 없으면 변경이 점별 방식으로 확산된다고 칩니다. 정확히 작업하려면 needed() 함수가 필요한 경우 이는 항상 오류입니다. MatteRGBA 예제 같은 예외도 있지만 보통 needed()와 changed()를 쌍으로 정의해야 합니다.
한층 복잡한 예제
RotateAndComposite는 한 입력을 변형하고 그 위의 것과 섞는 커널입니다. 여러 입력이 차별 대우받는 것이나 changed() 같은 한층 복잡한 영역 함수를 쓴 것을 보세요. 미리 계산한 변환 행렬을 담기 위해 종속 변수도 씁니다. 행렬들은 행 우선 정렬(column-major order)입니다. 자세한 건 픽셀 벤더 레퍼런스를 보세요.
3장 막판에 나온 코드 아닌가 -.-
<languageVersion : 1.0;>
kernel RotateAndComposite
<
namespace : "Tutorial";
vendor : "Adobe";
version : 1;
>
{
parameter float theta; // 회전각
parameter float2 center // 회전 중심
<
minValue: float2(0);
maxValue: float2(1000);
defaultValue: float2(200);
>;
dependent float3x3 back_xform; // 회전 행렬
dependent float3x3 fwd_xform; // 회전 행렬의 역행렬
input image4 foreground;
input image4 background;
output pixel4 result;
// 변환 행렬과 변환 행렬의 역행렬을 계산합니다
void evaluateDependents()
{
// 중심을 원점으로 옮깁니다
float3x3 translate = float3x3(
1, 0, 0,
0, 1, 0,
-center.x, -center.y, 1 );
// theta만큼 돌립니다
float3x3 rotate = float3x3(
cos(theta), sin(theta), 0,
-sin(theta), cos(theta), 0, 0, 0, 1 );
// 완전한 변환 행렬을 얻기 위해 곱합니다
fwd_xform = -translate*rotate*translate;
// 역회전을 얻습니다 (sin의 부호를 반대로 하여)
rotate[0][1] = -rotate[0][1];
rotate[1][0] = -rotate[1][0];
// 완전한 역행렬을 얻기 위해 곱합니다
back_xform = translate*rotate*-translate;
}
// needed 함수는 입력에 따라 다르게 작동합니다
region needed(region outputRegion, imageRef inputIndex )
{
region result;
if( inputIndex == background ) {
// background는 변형되지 않으니 그냥 넘어갑니다
result = outputRegion;
}
else {
// 출력 영역을 전경 공간으로 변형시킵니다
result = transform( back_xform, outputRegion );
}
return result;
}
// changed 함수는 needed 함수와 비슷하지만 변환 작업이 다릅니다
region changed(region inputRegion, imageRef inputIndex )
{
region result;
if( inputIndex == background ) {
result = inputRegion;
}
else {
result = transform( fwd_xform, inputRegion );
}
return result;
}
// outCoord를 변환하여 전경 픽셀을 알아내는 주요 함수
void evaluatePixel()
{
// 후경 좌표는 그냥 목적 좌표입니다
float2 bg_coord = outCoord();
// 전경 좌표는 outCoord를 float3으로 격상, 변환한 것이고 휘젓기를 통해 w를 버립니다
// 아핀 변환(Affine transformation)을 검색해보세요
float2 fg_coord =
(back_xform*float3(bg_coord.x, bg_coord.y, 1)).xy;
// 후경과 전경의 알파 혼합
pixel4 bg_pixel = sampleNearest(background, bg_coord);
pixel4 fg_pixel = sampleLinear(foreground, fg_coord);
result = mix(bg_pixel, fg_pixel, fg_pixel.a);
}
}
생성역 계산하기
generated() 함수는 입력이 텅텅 비었어도 픽셀을 만들어내야 하는 출력 영역을 기술합니다. 이 제너레이터 커널인 RenderFilledCircle의 경우처럼, 커널이 출력에 무언가를 그릴 때마다 정확히 작업하기 위해 generated()가 필요합니다.
kernel RenderFilledCircle
<...>
{
parameter float radius;
parameter pixel4 color;
output pixel4 result;
region generated()
{
float r = ceil(radius);
return region(float4(-r, -r, r, r));
}
void evaluatePixel()
{
float2 coord_for_this_pixel = outCoord();
float cur_radius = length(coord_for_this_pixel);
if (cur_radius < radius)
result = color;
else
result = pixel4(0,0,0,0);
}
}
generated() 함수는 인자가 없지만 결과를 계산하려면 어느 커널 매개변수에든 접근해야 하는 게 보통입니다. 이 경우는 radius 매개변수를 사용하여 출력의 경계 상자를 결정하며 이 경계 상자는 (왼쪽X, 위Y, 오른쪽X, 아래Y) 꼴로 영역 객체를 초기화하는 데 쓰입니다.
이 예제에는 입력 그림이 없지만, 입력을 하나 이상 처리하는 커널에서도 generated() 함수가 필요할 수 있습니다. 붓선이라던가 렌즈 광원 같이 절차상 그린 것을 입력에 덧씌울 때 필요하곤 합니다.
generated() 함수는 그린 개체를 포함하는 영역을 반환해야 합니다. 픽셀 벤더 런타임 엔진은 개체가 입력 그림의 경계를 삐져나간 곳에 맞춰 출력 버퍼를 확장하여 개체를 완전히 포함하는 고마운 행동은 하지 않습니다.
절차 질감 생성처럼 이론상 출력을 무한 평면 위에 만들어내는 커널은 내장 함수 everywhere()로 무한 영역을 반환해야 합니다.
애프터 이펙트 주석: 애프터 이펙트는 무한 영역 발생기인 everywhere()를 지원하지 않습니다. 커널이 이 내장 함수를 사용하는 데 애프터 이펙트에서 쓰고 싶으면 제한된 영역 안에서 출력을 만들게 고쳐야 합니다.
5 픽셀 벤더 그래프 언어
이 장에서는 픽셀 벤더 그래프 언어를 소개합니다. 이 언어를 이용하여 여러 픽셀 벤더 커널을 한 처리 그래프에다 연결하여 복잡한 그림 처리 효과를 만들 수 있습니다. 그래프는 한 필터처럼 다룰 수 있습니다.
그래프를 실행하면 픽셀 벤더 런타임 엔진은 입력 그림을 사슬의 첫 커널에 넘깁니다. 커널은 출력 그림을 완성하기 위한 모든 픽셀을 계산한 뒤 다음 커널의 입력 그림으로서 넘겨서 완료를 향해 달립니다. 커널은 원하는 출력 그림을 만들 때까지 계속 커널 사슬을 통과합니다. 효율성을 위해 픽셀 벤더 런타임 엔진은 먼저 모든 필요 입력 영역과 출력 영역(중간 그림의 것들 포함)을 결정하여 마지막 출력 그림에 전혀 필요 없는 픽셀들을 처리하지 않습니다.
픽셀 벤더는 그래프를 제작하고 편집하는 데 쓸 수 있는 그래프 편집기를 제공합니다. "그래프 편집기 사용하기"를 보세요.
플래시 주석: 플래시 플레이어에서는 그래프를 쓸 수 없습니다.
그래프 요소들
픽셀 벤더 그래프 언어는 그래프 구조를 기술하는 XML 기반 언어입니다. 마디들을 선언하고, 마디들 사이의 연결을 기입하고, 매개변수를 제공할 수 있습니다.
최상위 수준 컨테이너인 graph 요소는 다음 부분들을 포함합니다.
그래프 헤더 | graph 요소 속성들은 그래프의 이름을 포함하여 기본 헤더 정보를 제공합니다. 애플리케이션은 보통 이 값을 결과 필터의 제목으로 씁니다. |
그래프 메타데이터 | 이름공간과 그래프 판(version) 정보를 제공하는 메타데이터 요소 모음. |
그래프 매개변수 | 선택 사항. 제한(선택 사항) 아래 사용자가 입력한 이름 있는 값을 제공하는 매개변수 요소 모음. |
그림 입력과 그림 출력 | 입력 그림과 출력 그림을 명기하는 입력 요소와 출력 요소. |
끼워진 커널 | 픽셀 벤더로 작성한 하나 이상의 완전한 커널 정의. |
마디 | 끼워진 커널들 중 하나의 유일한 인스턴스를 저마다 명기하는 하나 이상의 마디 요소들. |
연결 | 입력과 출력 사이의 일련의 마디들을 명기하는 연결 요소 모음. |
간단한 그래프 예제
이렇게 확인 마디 하나만 있고 입력 그림과 출력 그림을 잇는 그래프가 가장 간단할 겁니다.
그래프 헤더
graph 요소는 최상위 수준 컨테이너입니다. 그래프의 이름, 사용한 픽셀 벤더 그래프 언어의 판, XML 이름공간이 속성으로 들어갑니다.
<?xml version="1.0" encoding="utf-8"?>
<graph name = "Simplest"
languageVersion = "1.0"
xmlns = "http://ns.adobe.com/PixelBenderGraph/1.0">
그래프 메타데이터
<metadata name = "namespace" value = "GraphTest" />
<metadata name = "vendor" value = "Adobe" />
<metadata name = "version" type = "int" value = "1" />
모든 픽셀 벤더 그래프와 커널은 이름, 이름공간(namespace), 제작자(vendor), 판의 유일한 조합으로 식별됩니다. 그래프의 이름은 graph 요소에서 이미 줬고, metadata 요소는 이름공간과 제작자와 판을 제공합니다. 이 모든 식별 항목은 반드시 공급해야 하고 공급하지 않으면 그래프는 컴파일되지 않습니다. version 메타데이터에는 int 타입 붙이는 것 주의하세요.
그래프 속 커널마다 그래프 이름공간과 구별되는 고유의 이름공간이 있습니다. "끼워진 커널"을 보세요.
그래프 매개변수
이 간단한 그래프에는 매개변수가 없기 때문에 이 부분은 비었습니다. "그래프 매개변수와 커널 매개변수 정의하기"에서 그래프 매개변수의 예를 보세요.
그림 입력과 그림 출력
<inputImage type = "image4" name = "graphSrc" />
<outputImage type = "image4" name = "graphDst" />
이 그래프는 "graphSrc"라는 4 채널 입력 그림 하나를 받아 "graphDst"라는 4 채널 출력 그림 하나를 만듭니다.
픽셀 하나를 만드는 커널과 달리 그래프는 그림 하나를 만듭니다.
▶ 커널의 출력은 픽셀 하나입니다. 픽셀 벤더 런타임 엔진은 요구 출력 영역의 모든 픽셀에 대해 커널을 실행하여 그림을 완성합니다.
▶ 그래프의 출력은 끼워진 커널들을 실행하여 생산한 그림 하나입니다. 그래프는 그림을 하나 출력해야 합니다.
끼워진 커널
끼워진 커널은 완전히 기술된 커널이며, 픽셀 벤더 그래프용 XML 코드 안에 포함된 픽셀 벤더 커널 언어 구문에 의해 생성됩니다.
<kernel>
<![CDATA[
<languageVersion : 1.0;>
kernel Identity
< namespace: "Tutorial";
vendor: "Adobe";
version: 1;
>
{
input image4 src;
output pixel4 dst;
void evaluatePixel() {
dst = sampleNearest(src, outCoord());
}
}
]]>
</kernel>
이 그래프는 픽셀 벤더 커널 언어로 작성한 끼워진 커널 하나를 포함합니다.
▶ 커널은 그래프처럼 고유의 이름, 이름공간, 제작자, 판 메타데이터가 있으며 각 커널을 유일하게 식별하기 위해 쓰입니다.
▶ 커널 매개변수, 종속 변수, 입력 그림, 출력 픽셀은 커널 이름공간 안에서 정의됩니다. 여기서 이름공간 값은 그래프 메타데이터에 쓰인 값과 같지만(그래프에는 GraphTest라고 써있고 여긴 Tutorial이라고 써있다. 오타인 듯)이 이름공간은 이 커널에게 여전히 유일한데, 커널의 다른 식별 특성들과 결합하여 구분되기 때문입니다.
마디
마디는 특정 커널의 인스턴스를 정의하며, 그래프 연결에 의해 정해진 순서에서 그 커널의 처리 과정이 적용됩니다.
<node id = "identity"
name="Identity" vendor="Adobe"
namespace="Tutorial" version="1" />
이 그래프는 마디가 하나며 마디는 유일한 마디 식별자인 identity를 지녔습니다. 이 그래프가 발동하는 커널은 고유의 이름과 메타데이터 속성들에 의해 식별됩니다. 이 경우 끼워진 커널로 정의된 Identity 커널이 발동됩니다.
보통 마디 ID는 자신이 발동하는 커널과 연관되고 유일하게 정해집니다. 예를 들어 이 예제에서처럼 마디 ID를 커널 이름과 구별하기 위해 대소문자를 다르게 할 수 있습니다. identity 마디는 Identity 커널을 호출합니다. 예시를 원하면 "여러 마디 정의하기"를 보세요.
연결
결국 그래프의 입력들, 출력들, 마디들이 연결되어 그래프를 완성합니다.
<connect fromImage = "graphSrc" toNode = "identity" toInput = "src" />
<connect fromNode ="identity" fromOutput = "dst" toImage = "graphDst" />
이 그래프에는 연결이 두 개 뿐입니다.
▶ 그래프의 입력 그림(graphSrc)은 myNode 마디(src)의 입력 그림에 연결됩니다.
▶ identity 마디(dst)의 출력 픽셀은 그래프의 출력 그림(graphDst)에 연결됩니다.
이 그림 변수들에 할당되는 이름은 서로 다른 이름공간(그래프 이름공간과 커널 이름공간) 안에 있으므로 같을 수도 있습니다. 문맥에서 구별하는 걸 돕기 위해 변수들에 다른 이름을 줬습니다.
▶ 그래프의 입력 그림과 출력 그림은 그래프의 inputImage 구문과 outputImage 구문에 의해 정의됩니다.
<!-- 그림 입력과 그림 출력 -->
<inputImage type = "image4" name = "graphSrc" />
<outputImage type = "image4" name = "graphDst" />
▶ 마디의 입력 그림과 출력 픽셀은 커널 정의의 일부입니다.
<kernel>
<![CDATA[
<languageVersion : 1.0;>
kernel Identity
< ... >
{
input image4 src;
output pixel4 dst;
void evaluatePixel()
{ ... }
}
]]>
</kernel>
진짜 간단한 예제
여기 Simplest 그래프를 정의하는 완전한 프로그램이 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<graph name = "Simplest"
languageVersion = "1.0"
xmlns="http://ns.adobe.com/PixelBenderGraph/1.0">
<!-- 그래프 메타데이터 -->
<metadata name = "namespace" value = "Graph Test" />
<metadata name = "vendor" value = "Adobe" />
<metadata name = "version" type = "int" value = "1" />
<!-- 그래프 매개변수 (이 예제에는 없습니다)-->
<!-- 그림 입력과 그림 출력 -->
<inputImage type = "image4" name = "graphSrc" />
<outputImage type = "image4" name = "graphDst" />
<!-- 끼워진 커널 -->
<kernel>
<![CDATA[
<languageVersion : 1.0;>
kernel Identity
< namespace: "Tutorial";
vendor: "Adobe";
version: 1;
>
{
input image4 src;
output pixel4 dst;
void evaluatePixel()
{
dst = sampleNearest(src, outCoord());
}
}
]]>
</kernel>
<!-- 마디 -->
<node id = "identity" name="Identity" vendor="Adobe"
namespace="Tutorial" version="1" />
<!-- 연결 -->
<connect fromImage = "graphSrc" toNode = "identity" toInput = "src" />
<connect fromNode ="identity" fromOutput = "dst" toImage = "graphDst" />
</graph>
복잡한 그래프
그래프 언어의 힘은 여러 커널을 하나로 묶는 능력에서 나옵니다. 특정 커널 작업은 여러 번 실행될 수 있습니다. 각 호출마다 매개변수 설정이 다르면 그래프에서 각각 유일한 마디로 표현됩니다.
이렇게 여러 커널을 묶어서 간단한 발광(glow) 필터를 구성할 수 있습니다.
이 그래프에는 흥미로운 점이 여럿 보입니다.
▶ 입력 그림인 graphSrc(그래프의 입력으로서 선언됨)은 필터 사슬에 의해 처리됩니다. 이는 어떻게 한 작업의 결과를 다른 작업에 투입하는가를 설명합니다.
▶ 필터 사슬의 결과는 수정되지 않은 graphSrc와 섞입니다. 이는 어떻게 마디에 여러 그림을 넘기는가를 설명합니다.
▶ 처리 과정에서 한 필터(BoxBlur)가 입력 매개변수(한 번은 수평 흐림, 다음은 수직 흐림)를 달리하여 두 번 쓰입니다. 이는 어떻게 한 마디가 필터 작업의 특정 인스턴스인지 설명합니다.
▶ 그래프 자체도 매개변수를 받으며 내부의 마디들도 매개변수를 받습니다. 어떻게 여러 정황에서 매개변수를 선언하고, 넘기고, 설정하고, 얻는가를 설명합니다.
그래프 정의하기
이 그래프의 완전한 XML 기술은 "진짜 복잡한 예제"에 있습니다. 그 예제는 간단한 예제에 있던 모든 부분을 포함합니다.
▶ 이 그래프를 "SimpleGlow"로 이름짓는 그래프 헤더와 메타데이터.
▶ graphSrc라는 입력 그림과 graphDst라는 출력 그림.
이 그래프는 추가로 다음을 정의합니다.
▶ 그래프를 실행할 때 사용자가 설정하는 그래프 매개변수.
▶ 그래프가 쓰는 세 커널 SelectBright, BoxBlur, Blend을 위한 끼워진 커널 정의. 이 커널들은 매개변수를 받습니다.
▶ 그래프 매개변수를 사용하여 자신이 실행하는 커널에 매개변수를 장착하는 여러 마디.
▶ 마디들 사이의 연결 그리고 한 마디에 대한 여러 입력을 정의하는 연결.
간단한 예제와 다른 부분들을 자세히 보겠습니다.
그래프 매개변수와 커널 매개변수 정의하기
이 그래프에는 amount라는 부동소수점 매개변수 하나가 있습니다.
<parameter type = "float" name = "amount" >
<metadata name = "defaultValue" type = "float" value = "5.0"/>
<metadata name = "maxValue" type = "float" value = "10.0" />
<metadata name = "minValue" type = "float" value = "0.0" />
</parameter>
픽셀 벤더 커널의 매개변수처럼 그래프 매개변수에도 통제를 기술하는 메타데이터를 넣을 수 있습니다. 이 경우 최솟값, 최댓값, 기본값이 있습니다.
커널도 매개변수를 받아들입니다.
▶ SelectBright는 밝기 한계 매개변수를 받습니다.
kernel SelectBright
< ... >
{
...
parameter float threshold;
void evaluatePixel() {
... 한계값을 이용하는 코드 ...
}
}
▶ BoxBlur는 direction 매개변수와 radius 매개변수를 받습니다.
kernel BoxBlur
< ... >
{
...
parameter float2 direction;
parameter int radius;
void evaluatePixel() {
... direction과 radius를 이용하는 코드 ...
}
}
그래프 매개변수는 그래프의 어느 마디에서든 쓸 수 있습니다. "커널 매개변수 설정하기"에서 볼 수 있듯 마디들은 그래프 매개변수를 이용하여 커널 매개변수를 설정합니다.
여러 마디 정의하기
이 그래프에는 마디가 넷입니다. 둘은 한 커널의 인스턴스입니다. 각 마디는 자신이 실행하는 커널의 이름과 다르고 유일한 마디 ID를 가집니다. 이 예제에서는 마디와 커널을 구별하기 유용한 이름짓기 규약을 설명합니다.
▶ selectBright 마디는 SelectBright 커널을 실행합니다
▶ boxblur1 마디와 boxblur2 마디 둘 다 BoxBlur 커널을 실행합니다. 각 인스턴스는 커널 매개변수 값을 다르게 설정합니다.
Blend 커널을 실행하는 마디는 다른 규약으로 이름을 짓습니다. 마디 이름인 composite는 한 그림을 두 입력 그림과 섞는 이 상황에서 혼합을 사용하는 목적을 반영합니다. 커널 작업을 그래프에서 다른 목적으로 적용할 수 있다면 이 규약을 사용하는 것이 괜찮습니다.
커널 매개변수 설정하기
매개변수들이 필요한 커널에게 마디는 해당 매개변수들의 값을 설정해줘야 합니다. 마디는 자신이 실행하는 커널의 모든 매개변수를 설정해야 합니다. 기본값이 있더라도요.
마디 정의에 evaluateParameters() 함수를 정의하는 evaluateParameters 요소를 포함시켜 이 작업을 처리합니다. (커널에 매개변수가 없으면 evaluateParameters 요소는 빼버립니다.)
evaluateParameters() 함수에서는 그래프 매개변수에 직접 접근하고 마디이름::매개변수이름 구문을 사용하여 커널 매개변수를 참조할 수 있습니다.
예컨데 selectBright 마디 정의는 이렇습니다.
<node
id = "selectBright"
name="SelectBright" vendor="Adobe"
namespace="Tutorial" version="1" >
<evaluateParameters>
<![CDATA[
void evaluateParameters() {
selectBright::threshold = 1.0 - (1.0 / amount);
}
]]>
</evaluateParameters>
</node>
두 BoxBlur 마디는 direction 매개변수를 달리 기입합니다. 첫 번째는 수평 흐림을, 두 번째는 수직 흐림을 수행합니다.
복잡한 연결 정의하기
이 그래프에는 연결이 여섯 개 있습니다.
▶ 첫번째 연결은 그래프의 입력 그림에서 selectBright 마디의 단독 입력 그림으로입니다.
<connect fromImage="graphSrc" toNode="selectBright" toInput="src"/>
▶ 다음 두 연결에서는 각 마디의 출력 그림을 다음 마디에 입력 그림으로서 넘깁니다.
<connect fromNode="selectBright" fromOutput="dst"
toNode="boxblur1" toInput="src"/>
<connect fromNode="boxblur1" fromOutput="dst"
toNode = "boxblur2" toInput="src"/>
▶ composite 마디는 입력이 orig, glow 두 개입니다. 하나는 그래프의 입력 그림에 의해 공급되고 다른 입력은 마지막 흐림 작업의 결과물입니다.
<connect fromImage="graphSrc"
toNode="composite" toInput="orig" />
<connect fromNode="boxblur2" fromOutput="dst"
toNode="composite" toInput="glow"/>
▶ 끝으로 마지막 합성 작업의 결과물이 그래프의 출력 그림에 넘어갑니다.
<connect fromNode="composite" fromOutput="dst" toImage="graphDst"/>
이는 어떻게 홑값을 여러 입력에 연결하는가를 보여줍니다. 그래프의 입력 그림은 selectBright 마디에도, composite 마디에도 연결됩니다.
진짜 복잡한 예제
여기 SimpleGlow 그래프를 정의하는 완전한 프로그램이 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<graph name="SimpleGlow"
languageVersion="1.0"
xmlns="http://ns.adobe.com/PixelBenderGraph/1.0">
<!-- 그래프 메타데이터 -->
<metadata name="namespace" value="Graph Test" />
<metadata name="vendor" value="Adobe" />
<metadata name="version" type="int" value="1" />
<!-- 그래프 매개변수 -->
<parameter type="float" name="amount" >
<metadata name="defaultValue" type="float" value="5.0"/>
<metadata name="maxValue" type="float" value="10.0" />
<metadata name="minValue" type="float" value="0.0" />
</parameter>
<!-- 그림 입력과 그림 출력 -->
<inputImage type="image4" name="graphSrc" />
<outputImage type="image4" name="graphDst" />
<!-- 끼워진 커널 -->
<kernel>
<![CDATA[
<languageVersion : 1.0;>
kernel SelectBright
<
namespace: "Tutorial";
vendor: "Adobe";
version: 1;
>
{
input image4 src;
output float4 dst;
parameter float threshold;
void evaluatePixel() {
float4 sampledColor=sampleNearest(src, outCoord());
// step(x, y) : x > y면 0.0, 아니면 1.0을 반환함
// dot(x, y) : x, y를 내적함
sampledColor.a *= step(threshold,
dot(sampledColor.rgb, float3(0.3, 0.59, 0.11)));
dst=sampledColor;
}
}
]]>
</kernel>
<kernel>
<![CDATA[
<languageVersion : 1.0;>
kernel BoxBlur
<
namespace: "Tutorial";
vendor: "Adobe";
version: 1;
>
{
input image4 src;
output float4 dst;
parameter float2 direction;
parameter int radius;
void evaluatePixel() {
float denominator=0.0;
float4 colorAccumulator=float4(0.0, 0.0, 0.0, 0.0);
float2 singlePixel=pixelSize(src);
colorAccumulator +=sampleNearest(src, outCoord());
for(int i=0; i <=radius; ++i) {
colorAccumulator +=sampleNearest(src,
outCoord()-(direction * singlePixel * float(i)));
colorAccumulator +=sampleNearest(src,
outCoord()+(direction * singlePixel * float(i)));
}
dst=colorAccumulator / (2.0 * float(radius) + 1.0);
}
region needed( region output_region, imageRef input_index ) {
region result = output_region;
result = outset( result,
( pixelSize(src) * direction ) * float( radius ) );
return result;
}
region changed( region input_region, imageRef input_index ) {
region result = input_region;
result = outset( result,
( pixelSize(src) * direction ) * float( radius ) );
return result;
}
}
]]>
</kernel>
<kernel>
<![CDATA[
<languageVersion : 1.0;>
kernel Blend
<
namespace: "Tutorial";
vendor: "Adobe";
version: 1;
>
{
input image4 orig;
input image4 glow;
output float4 dst;
void evaluatePixel() {
float4 sourcePixel=sampleNearest(orig, outCoord());
float4 blurredGlowPixel=sampleNearest(glow,outCoord());
sourcePixel.rgb=mix(
blurredGlowPixel.rgb, sourcePixel.rgb,
blurredGlowPixel.a);
dst=sourcePixel;
}
}
]]>
</kernel>
<!-- 마디 -->
<node
id="selectBright"
name="SelectBright"
vendor="Adobe"
namespace="Tutorial"
version="1" >
<evaluateParameters>
<![CDATA[
void evaluateParameters() {
selectBright::threshold=1.0 - (1.0 / amount);
}
]]>
</evaluateParameters>
</node>
<node
id="boxblur1"
name="BoxBlur" vendor="Adobe"
namespace="Tutorial" version="1" >
<evaluateParameters>
<![CDATA[
void evaluateParameters() {
boxblur1::direction=float2(1.0, 0.0);
boxblur1::radius=int(ceil(amount));
}
]]>
</evaluateParameters>
</node>
<node
id="boxblur2"
name="BoxBlur" vendor="Adobe"
namespace="Tutorial" version="1" >
<evaluateParameters>
<![CDATA[
void evaluateParameters() {
boxblur2::direction=float2(0.0, 1.0);
boxblur2::radius=int(ceil(amount));
}
]]>
</evaluateParameters>
</node>
<node
id="composite"
name="Blend"
vendor="Adobe"
namespace="Tutorial"
version="1" >
</node>
<!-- 연결 -->
<connect fromImage="graphSrc"
toNode="selectBright" toInput="src"/>
<connect fromImage="graphSrc"
toNode="composite" toInput="orig" />
<connect fromNode="selectBright" fromOutput="dst"
toNode="boxblur1" toInput="src"/>
<connect fromNode="boxblur1" fromOutput="dst"
toNode="boxblur2" toInput="src"/>
<connect fromNode="boxblur2" fromOutput="dst"
toNode="composite" toInput="glow"/>
<connect fromNode ="composite" fromOutput="dst"
toImage="graphDst"/>
</graph>
그래프 편집기 사용하기
픽셀 벤더 툴킷에는 커널 편집기와 같은 방식으로 작동하는 그래프 편집기가 들었습니다.
▶ 이미 있는 그래프 프로그램을 열려면 File > Open Filter를 고르고 PBG 파일을 고르세요.
▶ 새 그래프 프로그램을 위한 기본 코드 뼈대를 얻으려면 File > New Graph를 고르거나 "Create a new graph"를 누르세요.
그래프 프로그램을 수정하는 것은 커널 프로그램을 수정하는 것과 다를 바가 없습니다. 프로그램을 실행할 때 필터 매개변수 UI로 커널 매개변수를 설정하는 것과 같은 방식으로 그래프 매개변수를 설정할 수 있습니다.
6 애프터 이펙트용으로 개발하기
이 장에서는 픽셀 벤더 프로그램을 애프터 이펙트에서 이펙트로서 쓰는 것에 중점을 두고 몇 기법을 소개합니다. 여기의 모든 예제는 애프터 이펙트용이라 4 채널 입력과 출력을 사용합니다.
애프터 이펙트 커널 메타데이터
▶ 애프터 이펙트는 두 커널 메타데이터 속성을 추가로 정의합니다. 둘 다 선택 사항입니다.
displayname | Effects and Presets 패널에 보일 이펙트 이름. 기입하지 않으면 커널 이름이 쓰입니다. |
category | 이펙트 분류. 기본값은 'Pixel Bender' 분류입니다. |
여기 부수적인 메타데이터 값을 추가하고 4 채널 입력 그림과 출력 그림만 써서 애프터 이펙트에 맞춘 간단한 확인 커널이 있습니다.
<languageVersion : 1.0;>
kernel Identity
<
namespace: "After Effects";
vendor: "Adobe Systems Inc.";
version: 1;
description: "This is an identity kernel";
displayname: "Identity Kernel";
category: "PB Effects";
>
{
input image4 source;
output pixel4 result;
void evaluatePixel() {
result = sampleNearest(source, outCoord());
}
}
4 채널 값에 접근하기
▶ 애프터 이펙트에서는 4 채널 입력 그림과 출력 그림만 쓸 수 있습니다.
첫번째 연습에서처럼 evaluatePixel() 함수를 고쳐 입력 그림의 여러 채널 값을 조절합니다.
void evaluatePixel() {
float4 temp = sampleNearest(source, outCoord());
result.r = 0.5 * temp.r;
result.g = 0.6 * temp.g;
result.b = 0.7 * temp.b;
result.a = temp.a;
}
이는 어떻게 점 연산자 뒤에 채널 이름의 머리글자를 써서 여러 채널에 접근하는가를 보여줍니다. 이 예제는 빛깔 채널만 작업하고 투명도(알파 채널)는 바꾸지 않습니다.
이것은 여러 채널에 접근하는 다른 방법으로, 한 작업으로 모든 빛깔을 조절합니다.
void evaluatePixel() {
float4 temp = sampleNearest(source, outCoord());
float3 factor = float3(0.5, 0.6, 0.7);
result.rgb = factor * temp.rgb;
result.a = temp.a;
}
이 코드를 더 줄일 수 있습니다.
void evaluatePixel() {
float4 temp = sampleNearest(source, outCoord());
result = float4( 0.5, 0.6, 0.7, 1.0) * temp;
}
이 경우 채널들이 분명히 드러나지 않으므로 temp와 result의 모든 채널이 이용됩니다.
회선 예제
회선은 널리 쓰이는 그림 처리 작업입니다. 원그림과 회선 필터나 회선 덮개라 부르는 더 작은 그림 사이의 곱의 합을 계산하여 그림을 조작합니다. 회선 덮개에서 고른 값들에 따라 부드럽게 하기, 날카롭게 하기, 경계 검출 외 유용한 표현 작업을 수행할 수 있습니다.
이 장에서는 예제를 제작하여 애프터 이펙트에 한정된 픽셀 벤더의 몇 특성을 살펴보겠습니다.
이 예제는 회선으로 그림을 부드럽게 만드는 것으로 시작합니다. 그저 덮개에 포함된 픽셀들을 평균내어 그림을 부드럽게 할 수 있습니다. 부드럽게 하기 위한 필터 덮개는 반경 1인 3x3 덮개입니다.
[1, 1, 1]
[1, 1, 1]
[1, 1, 1]
커널은 이 덮개를 3 x 3 상수인 smooth_mask로서 정의합니다.
const float3x3 smooth_mask = float3x3( 1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0);
사용자가 정의한 함수 convolve()는 핵심 알고리즘을 구현합니다. 이 함수는 입력 그림 전체에 대해 smooth_mask 배열을 이용해 회선 작업을 합니다. 그리고 결과를 9(덮개의 계수들의 합)로 나눠 평균냅니다.
float4 convolve(float3x3 in_kernel, float divisor) {
float4 conv_result = float4(0.0, 0.0, 0.0, 0.0);
float2 out_coord = outCoord();
for(int i = -1; i <= 1; ++i) {
for(int j = -1; j <= 1; ++j) {
conv_result += sampleNearest(source,
out_coord + float2(i, j)) * pixelSize(src) * in_kernel[i + 1][j + 1];
}
}
conv_result /= divisor;
return conv_result;
}
끝으로 evaluatePixel() 함수는 convolve() 함수를 호출하여 결과를 커널의 출력 픽셀로 삼습니다.
void evaluatePixel() {
float4 conv_result = convolve(smooth_mask, smooth_divisor);
result = conv_result;
}
다음은 필터 덮개로 출력 그림에 회선 작업을 수행하는 완전한 커널입니다.
<languageVersion : 1.0;>
kernel ConvKernel
<
namespace: "After Effects";
vendor : "Adobe Systems Inc.";
version : 1;
description : "Convolves an image using a smoothing mask";
>
{
input image4 source;
output pixel4 result;
const float3x3 smooth_mask = float3x3( 1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0);
const float smooth_divisor = 9.0;
float4 convolve(float3x3 in_kernel, float divisor) {
float4 conv_result = float4(0.0, 0.0, 0.0, 0.0);
float2 out_coord = outCoord();
for(int i = -1; i <= 1; ++i) {
for(int j = -1; j <= 1; ++j) {
conv_result += sampleNearest(source,
out_coord + float2(i, j)) * in_kernel[i + 1][j + 1];
}
}
conv_result /= divisor;
return conv_result;
}
void evaluatePixel() {
float4 conv_result = convolve(smooth_mask, smooth_divisor);
result = conv_result;
}
region needed( region output_region, imageRef input_index ) {
region result = output_region;
result = outset( result, float2( 1.0, 1.0 ) );
return result;
}
region changed( region input_region, imageRef input_index ) {
region result = input_region;
result = outset( result, float2( 1.0, 1.0 ) );
return result;
}
} // 커널 끝
애프터 이펙트에서의 커널 매개변수
애프터 이펙트는 고유의 매개변수 모음에 맞추기 위해 몇 매개변수 메타데이터를 추가로 정의합니다.
▶ aeDisplayName
커널 매개변수의 기본 표시 이름은 매개변수 이름입니다. 픽셀 벤더 매개변수 이름에는 공백이나 예약 문자가 들어갈 수 없기 때문에 이 메타데이터 값을 사용하여 친사용자 문자열을 입력할 수 있고 애프터 이펙트는 UI에서 이 매개변수가 나타나는 곳 어디에든 이 값을 사용할 것입니다.
parameter float myKernelParam
<
minValue: 0.0;
maxValue: 100.0;
defaultValue: 0.0;
aeDisplayName: "Pretty Display Name";
>;
▶ aeUIControl
이 매개변수 값으로 애프터 이펙트에다 이 매개변수에 대한 사용자 입력을 받기 위해 어느 종류의 UI 컨트롤을 사용해야 할지 알립니다. 허용된 값은 매개변수의 자료형마다 다릅니다.
매개변수 자료형 | 허용된 제어값 |
int | aeUIControl:"aePopup" 파이프 문자로 나뉘고 개별 항목 모음을 나타내는 문자열인 팝업 메뉴 값을 추가적인 메타데이터 값으로 기입해야 합니다. aePopupString:"Item 1|Item 2|Item 3" |
float | aeUIControl:"aeAngle" aeUIControl:"aePercentSlider" aeUIControl:"aeOneChannelColor" |
float2 | aeUIControl: "aePoint" 애프터 이펙트에게 이 매개변수가 그림에 그려질 원의 중심처럼 그림 속 한 점을 나타낸다고 알립니다. 기본점을 기입하려면 다음 추가적인 메타데이터 값을 사용하세요. aePointRelativeDefaultValue: float2(x, y) 위치는 픽셀 좌표가 아니라 그림의 크기에 상대적입니다. 예를 들어 aePointRelativeDefaultValue: float2(0.5, 0.5)라고 입력하면 그림의 크기가 얼마든 기본점이 그림의 중심으로 설정됩니다. 상대 좌표가 (1.0, 1.0)이면 그림의 오른쪽 아래, (0.0, 0.0)이면 왼쪽 위로 기본점이 설정됩니다. |
float3 float4 |
aeUIControl: "aeColor" 애프터 이펙트 CS4의 빛깔 컨트롤은 불투명 빛깔만 지원하기 때문에 float4 빛깔값에서 알파 채널은 항상 1입니다. |
이것은 애프터 이펙트 메타데이터를 사용하는 매개변수 예입니다. 이 매개변수를 회선 예제에 넣어서 회선 결과를 원래 그림과 섞을 수 있습니다.
parameter float blend_with_original
<
minValue: 0.0;
maxValue: 100.0;
defaultValue: 0.0;
description: "Amount to blend with original input"; // AE에서는 무시됨
aeDisplayName: "Blend Factor"; // AE 한정 메타데이터
aeUIControl: "aePercentSlider"; // AE 한정 메타데이터
>;
▶ 매개변수의 최솟값, 최댓값, 기본값을 제공하지 않으면 애프터 이펙트는 매개변수의 자료형에 알맞은 값을 고릅니다.
▷ 정수 매개변수면 최솟값 0, 최댓값 100, 기본값 0입니다.
▷ 실수 매개변수면 최솟값 0.0, 최댓값 1.0, 기본값 0.0입니다.
▶ description 값은 애프터 이펙트에서 쓰이지 않습니다. 코드에서 설명용으로 활용할 수는 있습니다.
▶ 애프터 이펙트는 UI에 매개변수를 표시하기 위해 aeDisplayName 값인 "Blend Factor"를 사용합니다.
aeDisplayName을 제공하지 않으면 매개변수 이름인 "blend_with_original"이 쓰입니다.
▶ 애프터 이펙트에게 이 매개변수를 퍼센트 슬라이더로 나타내도록 aeUIControl 값으로 알립니다. evaluatePixel() 함수 안에서 이 매개변수를 이용할 수 있습니다.
void evaluatePixel() {
float4 conv_result = convolve(smooth_mask, smooth_divisor);
result = mix(conv_result, sampleNearest(source, outCoord()), blend_with_original);
}
내장 함수 mix()는 원래 입력과 회선 결과 사이를 선형 보간합니다.
회선 커널 확장
회선 커널의 이 판에서는 새 매개변수를 추가하여 부드럽게 하기와 날카롭게 하기 중 한 작업을 고르도록 합니다. 이게 원래 회선 커널과 다른 점입니다.
▶ 날카롭게 하는 회선 덮개와 이것의 정규화 상수 (1.0)을 추가합니다.
▶ 새 정수 매개변수 (kernel_type)를 추가하여 부드럽게 하는 필터와 날카롭게 하는 필터 중 고르게 합니다.
▶ 핵심 함수 convolve()는 그대로입니다. evaluatePixel() 함수는 고른 필터 덮개를 convolve() 함수로 넘기기 위해 고쳤습니다.
<languageVersion : 1.0;>
kernel ConvKernel
<
namespace: "After Effects";
vendor : "Adobe Systems Inc.";
version : 1;
description : "Convolves an image using a smoothing or sharpening mask";
>
{
input image4 source;
output pixel4 result;
parameter int kernel_type
<
minValue: 0;
maxValue: 1;
defaultValue: 0;
aeDisplayName: “Kernel Type”;
aeUIControl: "aePopup";
aePopupString: "Smooth|Sharpen";
>;
parameter float blend_with_original
<
minValue: 0.0;
maxValue: 100.0;
defaultValue: 0.0;
aeDisplayName: "Blend Factor"; // 애프터 이펙트 한정 메타데이터
aeUIControl: "aePercentSlider"; // 애프터 이펙트 한정 메타데이터
>;
const float3x3 smooth_mask = float3x3( 1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0);
const float smooth_divisor = 9.0;
const float3x3 sharpen_mask = float3x3(-1.0, -1.0, -1.0,
-1.0, 9.0, -1.0,
-1.0, -1.0, -1.0);
const float sharpen_divisor = 1.0;
float4 convolve(float3x3 in_kernel, float divisor) {
float4 conv_result = float4(0.0, 0.0, 0.0, 0.0);
float2 out_coord = outCoord();
for(int i = -1; i <= 1; ++i) {
for(int j = -1; j <= 1; ++j) {
conv_result += sampleNearest(source,
out_coord + float2(i, j)) * in_kernel[i + 1][j + 1];
}
}
conv_result /= divisor;
return conv_result;
}
void evaluatePixel() {
float4 conv_result;
if (kernel_type == 0) {
conv_result = convolve(smooth_mask, smooth_divisor);
}
else {
conv_result = convolve(sharpen_mask, sharpen_divisor);
}
result = mix(conv_result, sampleNearest(source, outCoord()),
blend_with_original);
}
}
종속 함수
이 절에서는 어떻게 종속 함수를 정의하여 회선 커널을 최적화하는가를 설명합니다.
evaluatePixel() 함수는 각 출력 픽셀에 대해 호출되므로 지금 꼴로는 수행할 회선 종류 검사도 각 픽셀에 대해 수행됩니다. 이 값은 프레임 내내 그대로라서 덮개 먼저 계산할 수 있다면 픽셀 당 검사를 없앨 수 있습니다. 이걸 종속 멤버 변수와 evaluateDependents() 함수로 해결할 수 있습니다.
회선 덮개를 위한 나눗수도 그 덮개에 대한 추가적인 계수를 추가하여 계산할 수 있으므로 역시 더 빨리 계산할 수 있습니다. 이렇게 하면 커널이 확장하기 쉬워지며 나눗수를 걱정하지 않아도 다른 회선 덮개를 추가할 수 있습니다.
종속으로 선언된 변수들은 각 프레임마다 한 번씩만 실행되는 evaluateDependents()의 몸체에서 초기화됩니다. 그 후 이 값들은 읽기 전용이고 evaluatePixel()이 적용되는 모든 픽셀에 대해 상수입니다.
이전 회선 커널을 이렇게 수정할 겁니다.
▶ kernel_type 매개변수를 위한 새 항목, 팝업 목록과 함께 엠보스 회선 덮개를 추가합니다.
▶ 두 종속 변수 filter_mask와 mask_divisor를 추가하여 선택한 회선 덮개와 연관 나눗수를 저장합니다.
▶ evaluateDependents()를 정의하여 종속 변수 filter_mask에다 고른 회선 덮개를 복사합니다.
▶ 특정 덮개에 한정된 나눗수를 없애고, 새 evaluateDependents() 정의에 나눗수 계산을 추가합니다. 계산한 값을 종속 변수 mask_divisor에 저장합니다.
▶ evaluatePixel()을 두 종속 변수를 핵심 함수 convolve()에 넘기게 고칩니다.
다른 건 다 같습니다. 이제 kernel_type을 픽셀마다가 아니라 프레임마다 비교합니다.
여기 수정한 커널 코드입니다.
<languageVersion : 1.0;>
kernel ConvKernel
<
namespace: "After Effects";
vendor : "Adobe Systems Inc.";
version : 1;
description : "Convolves an image using a smooth, sharpen, or emboss mask";
>
{ //kernel starts
input image4 source;
output pixel4 result;
parameter int kernel_type
<
minValue: 0;
maxValue: 1;
defaultValue: 0;
aeDisplayName: “Kernel Type”;
aeUIControl: "aePopup";
aePopupString: "Smooth|Sharpen|Emboss";
>;
parameter float blend_with_original
<
minValue: 0.0;
maxValue: 100.0;
defaultValue: 0.0;
aeDisplayName: "Blend Factor"; // 애프터 이펙트 한정 메타데이터
aeUIControl: "aePercentSlider"; // 애프터 이펙트 한정 메타데이터
>;
const float3x3 smooth_mask = float3x3( 1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0);
const float3x3 sharpen_mask = float3x3(-1.0, -1.0, -1.0,
-1.0, 9.0, -1.0,
-1.0, -1.0, -1.0);
const float3x3 emboss_mask = float3x3( -2.0, -1.0, 0.0,
-1.0, 1.0, 1.0,
0.0, 1.0, 2.0);
dependent float3x3 filter_mask;
dependent float mask_divisor;
float findDivisor(float3x3 mask) {
float divisor = 0.0;
for(int i = 0; i < 3; ++i) {
for(int j = 0; j < 3; ++j) {
divisor += mask[i][j];
}
}
return divisor;
}
void evaluateDependents() {
if (kernel_type == 0) {
filter_mask = smooth_mask;
}
else if (kernel_type == 1) {
filter_mask = sharpen_mask;
}
else if (kernel_type == 2) {
filter_mask = emboss_mask;
}
mask_divisor = findDivisor(filter_mask);
}
float4 convolve(float3x3 in_kernel, float divisor) {
float4 conv_result = float4(0.0, 0.0, 0.0, 0.0);
float2 out_coord = outCoord();
for(int i = -1; i <= 1; ++i) {
for(int j = -1; j <= 1; ++j) {
conv_result += sampleNearest(source,
out_coord + float2(i, j)) * in_kernel[i + 1][j + 1];
}
}
conv_result /= divisor;
return conv_result;
}
void evaluatePixel() {
float4 conv_result = convolve(filter_mask, mask_divisor);
result = mix(conv_result, sampleNearest(source, outCoord()),
blend_with_original);
}
region needed( region output_region, imageRef input_index ) {
region result = output_region;
result = outset( result, float2( 1.0, 1.0 ) );
return result;
}
region changed( region input_region, imageRef input_index ) {
region result = input_region;
result = outset( result, float2( 1.0, 1.0 ) );
return result;
}
} // 커널 끝
7 플래시용으로 개발하기
이 장에서는 픽셀 벤더를 플래시, 플렉스, 액션스크립트에서 다루는 방법을 설명합니다.
픽셀 벤더 필터를 SWF에 끼워넣기
필터를 SWF에 끼워넣어 실행시간에 동적으로 불러오지 않아도 되게 합니다.
package
{
import flash.display.*;
import flash.events.*;
import flash.filters.*;
import flash.net.*;
import flash.utils.ByteArray;
// SWF 메타데이터
[SWF(width="600", height="400", backgroundColor="#000000", framerate="30")]
public class PB extends Sprite {
// 픽셀 벤더 커널의 이진 바이트들을 담은 파일
[Embed("testfilter.pbj", mimeType="application/octet-stream")]
private var TestFilter:Class;
// 필터를 먹여 표시할 그림
[Embed(source="image.jpg")]
private var image:Class;
private var im:Bitmap;
public function PB():void {
im = new image() as Bitmap;
addChild(im);
// 불러온 필터를 Shader 객체에 ByteArray 객체로서 넘깁니다
var shader:Shader = new Shader(new TestFilter() as ByteArray);
shader.data.amount.value = [100];
var filter:ShaderFilter = new ShaderFilter(shader);
// 필터를 그림에 먹입니다
im.filters = [filter];
}
}
}
픽셀 벤더 필터를 액션스크립트 라이브러리로 만들기
액션스크립트 클래스로 감싼 픽셀 벤더 필터를 포함하는 SWC(재배포 가능 액션스크립트 클래스 라이브러리)를 제작할 수 있습니다. 플렉스 빌더, MXMLC, 플래시 전문가용으로 개발하는 플래시 플레이어 프로젝트에서 사용할 수 있는 커스텀 픽셀 벤더 필터의 SWC 라이브러리를 제작하는 데 이 기법을 쓸 수 있습니다.
필터를 액션스크립트 클래스로 감싸기
이 간단한 예제에서는 커스텀 픽셀 벤더 필터를 액션스크립트 3 클래스로 감싸며 이 커스텀 필터는 다른 내장 필터처럼 쓰고 재사용할 수 있습니다.
문서 클래스: PBFilter.as
package
{
import flash.display.Bitmap;
import flash.display.Sprite;
// SWF 메타데이터
[SWF(width="600", height="400", backgroundColor="#000000", framerate="30")]
public class PBFilter extends Sprite
{
// 필터를 먹여 표시할 그림
[Embed(source="image.jpg")]
private var image:Class;
private var im:Bitmap;
public function PBFilter():void
{
im = new image() as Bitmap;
addChild(im);
var filter:TestFilter = new TestFilter();
filter.value = 30;
// 필터를 그림에 먹인다
im.filters = [filter];
}
}
}
필터 클래스: TestFilter.as
package
{
import flash.display.Shader;
import flash.filters.ShaderFilter;
import flash.utils.ByteArray;
public class TestFilter extends ShaderFilter
{
// 픽셀 벤더 필터의 이진 바이트들을 담은 파일
[Embed("testfilter.pbj", mimeType="application/octet-stream")]
private var Filter:Class;
private var _shader:Shader;
public function TestFilter(value:Number = 50)
{
// 끼워진 픽셀 벤더 필터로 ShaderFilter 객체를 초기화합니다
_shader = new Shader(new Filter() as ByteArray);
// 기본값을 설정합니다
this.value = value;
super(_shader);
}
// 이 필터에는 value라는 값 하나만 있습니다
public function get value():Number
{
return _shader.data.amount.value[0];
}
public function set value(value:Number):void
{
// 픽셀 벤더 필터는 값들의 배열을 취합니다.
// 이 예제에서는 값을 하나만 사용합니다.
_shader.data.amount.value = [value];
}
}
}
SWC 라이브러리 만들기
플렉스 빌더를 사용하거나 플렉스 SDK에 포함된 명령줄 도구인 compc를 사용하여 SWC를 만들 수 있습니다.
플렉스 빌더로
1. Window > Preferences > Flex > Installed Flex SDKs > Add로 플렉스 빌더에 플렉스 SDK를 추가하세요. 플렉스 SDK에는 필터 클래스를 제작하고 컴파일하는 데 필요한 클래스들이 들었습니다.
2. File > New > Flex Library Project로 "Flex Library Project"를 생성하세요.
3. 프로젝트 이름과 프로젝트를 저장할 위치를 기입하세요.
4. Flex SDK version 밑에서 "Use a specific SDK"를 누르고 방금 추가한 플렉스 SDK(아마 Flex 3.2일 겁니다)를 고르세요.
- 비트맵 데이터를 이용한 가변 지형 구현 2009.11.21
- getTimer()로 며칠까지 잴 수 있을까? 2009.10.21
- undefined와 null는 미묘하게 다르다 2009.10.21
- 픽셀 벤더 개발자 안내서 [1/2] 2009.10.07