CODEONWORT

Displacement Map Filter 연구 본문

Season 1/플래시

Displacement Map Filter 연구

codeonwort 2011.02.09 04:14

소스 코드에 대해서는 양심적으로 '이 코드 내가 작성하진 않았어' 만 생각해주시면 되며 그 외의 권리는 주장하지 않아요

DMF(Displacement Map Filter)는 대상 비트맵을  비트맵의 색상 배치에 따라 왜곡하는 필터다.

대상 비트맵(왼쪽)과 맵 비트맵(오른쪽)

맵 비트맵의 (i, j)픽셀의 색상 정보에 의해 대상 비트맵의 (i, j) 픽셀의 색상은 주변의 픽셀 색상으로 바뀐다. scaleX와 scaleY는 나중에 설명하겠지만 이 값들이 양수일 때, 맵의 (i, j) 픽셀의 색깔이 중간값보다 연할 수록 ↖쪽의 픽셀이 복사되고 진할 수록 ↘쪽의 픽셀이 복사된다.

맵의 픽셀 색깔을 쓴다고 했지만, 픽셀 색깔은 A(투명도), R(적색), G(녹색), B(청색) 이렇게 채널이 네 개인데 무슨 채널의 값을 쓸 것인가? 그래서 값을 얻을 채널을 하나 고르는데 이건 componentX, componentY와 관련있다. 각 채널의 값 범위는 0~255이고 중간값은 128이다. 위 그림의 산 이미지를 맵으로 쓰면(그림이 파란색 계열이니 채널로 B를 고른다) 중앙에서 수직 방향으로 멀어질 수록 그 위치에서 대상의 픽셀 왜곡이 심해진다. 청색이 중앙에서 위로 가면 연해지고 아래로 가면 진해져 둘 다 중간값과 멀어지기 때문이다.

왜곡 결과. 여기 보이는 모든 픽셀 색상은 원래 대상 비트맵에 있던 것들이며 다만 위치가 바뀌었을 뿐이다.
(이건 세로 방향으로만 왜곡을 적용한 결과다. 맵의 픽셀 색상에 따른 가로 왜곡은 적용하지 않았다)

단순히 중간값에서 멀어질 수록 왜곡이 심하다고 하였는데, 다음 공식은 색상을 가져오는 주변 픽셀의 위치를 정확히 나타낸 것이다.


componentX(x, y), componentY(x, y)는 맵의 (x, y) 픽셀의 색상 채널 값이다. ARGB 중 어느 채널의 값을 얻느냐는 DisplacementMapFilter 클래스의 componentX, componentY 속성으로 결정한다. 이 속성에 대입할 채널 값은 R=1, G=2 B=4 A=8 이다. 숫자를 직접 입력하지 않고 flash.display.BitmapDataChannel 클래스에 정의된 상수를 써도 된다.

componentX와 componentY에 같은 채널을 지정하지 않아도 된다. 즉 가로 왜곡에는 맵의 R(적색) 채널, 세로 왜곡에는 B(청색) 채널을 이용할 수도 있다. 이런 경우 맵의 모든 픽셀의 R 채널이 128이라면 대상 비트맵에서 세로 왜곡은 전혀 일어나지 않는다. 또한 색상이 중간값에서 멀어진 정도는 같아도 가로 왜곡의 정도와 세로 왜곡의 정도를 다르게 할 수 있다.

아무 가공도 하지 않은 그림을 맵으로 쓰면 왜곡 결과가 깔끔하지 않으며 무슨 결과가 나올지 예측하기도 어렵다. 그래서 마지막 예제인 <시각화 : 광기>처럼 흥미로운 결과가 나오기도 하지만, 굴절 같은 특정 효과를 내려면 적절한 왜곡 수식을 세우고 맵을 직접 칠해야 한다.

그럼 DMF를 실제로 쓰는 방법을 보고 예제를 몇 개 만들어보겠다.


componentX, componentY, scaleX, scaleY 는 위에서 설명했으니 넘어가고 나머지는 예제를 만들면서 살펴본다.


0. 기초 예제
위에서 예시로 본 그림 변형을 직접 해보자.

이건 완성 파일에서 코드만 제외한 파일이다.
* 모든 예제는 플래시 CS4 이상에서 열립니다

라이브러리를 보면 비트맵 이미지가 두 장 있고 석양은 Target, 푸른 언덕은 Map이라는 이름으로 액션스크립트 클래스로 내보내기를 해놓았다. 둘 다 크기는 400x300이다. 액션스크립트에서 쓰게 인스턴스를 생성한다. 이렇게 프레임 스크립트를 적는다.

var tar:BitmapData = new Target(400, 300)
var map:BitmapData = new Map(400, 300)

그리고 DMF 객체를 만든다.

var dmf:DisplacementMapFilter = new DisplacementMapFilter()
dmf.mapBitmap = map
dmf.mapPoint = new Point   // 이건 주석으로는 못 설명하겠고 밑에서 따로 설명
dmf.componentX = 1   // 가로 왜곡에는 맵 비트맵의 R 채널을 쓴다. 두 줄 밑에 scaleX를 보면 이건 필요가 없음을 알게 된다.
dmf.componentY = 4   // 세로 왜곡에는 맵 비트맵의 B 채널을 쓴다.
dmf.scaleX = 0   // 처음 나온 공식을 다시 보자. scaleX를 중간값과의 차이에 곱하는데 scaleX가 0이니 가로 왜곡은 일어나지 않는다.
dmf.scaleY = 200   // 세로 왜곡의 정도를 적당히 큰 수로 잡는다.

알아보기 쉽게 이렇게 나열했지만 레퍼런스에 나온 것처럼 생성자에서 모두 지정할 수도 있다. 그리고 위 그림을 보고 이 코드를 보면 mode, color, alpha가 빠진 게 보이는데 이건 나중에 설명한다.

var dmf:DisplacementMapFilter = new DisplacementMapFilter(map, new Point, 1, 4, 0, 200)

마지막으로 필터를 적용하고 화면에 표시한다.

tar.applyFilter(tar, tar.rect, new Point, dmf)
addChild(new Bitmap(tar))

applyFilter를 잠깐 보자.

applyFilter(source:BitmapData, sourceRect:Rectangle, destPoint:Point, filter:BitmapFilter):void

그림이 그려진 BitmapData 객체에 필터를 직접 적용하는 것이 아니고, applyFilter를 호출한 BitmapData 객체에다 필터를 적용한 결과를 그리는 것이다. 물론 한 BitmapData 객체가 applyFilter를 호출하는데 source도 이 객체로 지정하면 직접 적용되는 꼴이 된다.

필터를 적용할 비트맵이 있다. (source)

필터를 적용할 부분을 지정한다. (sourceRect)
* source 전체에 필터를 적용하려면 source.rect 라고 쓴다

applyFilter를 호출한 BitmapData 객체에다 필터를 적용한 결과를 그려야 하는데, 결과 그림의 좌상단 모서리가 위치할 좌표를 지정한다. (destPoint)

실행하면 이렇게 보인다.

 

마지막으로 DMF의 mapPoint 속성을 짚고 넘어가야겠다.

var dmf:DisplacementMapFilter = new DisplacementMapFilter()
dmf.mapBitmap = map
dmf.mapPoint = new Point
dmf.componentX = 1
dmf.componentY = 4
dmf.scaleX = 0
dmf.scaleY = 200

어도비 레퍼런스에는 '맵 이미지의 왼쪽 위 모서리로부터 대상 표시 객체의 왼쪽 위 모서리에 대한 오프셋을 포함하는 값' 이라고 쓰여 있지만 뭔 소린지 모르겠고 직접 값을 넣어 테스트해보자.


즉 맵을 그대로 그리고, mapPoint만큼 옮긴 자리에 필터를 적용한 결과를 그 위에 덮은 것처럼 그려지는데 이런 기능이 쓸모가 있는지 모르겠다;

이건 완성 파일이다.


1. 꺾기
 

스테이지를 보면 맵에 쓸 무비클립(grad)과 슬라이더(slider)가 보인다. 무비클립을 직접 맵으로 쓸 순 없으니 BitmapData 객체에 그리고 이 객체를 맵으로 쓴다. 슬라이더는 꺾이는 정도를 조절한다. 세로로만 꺾이므로 scaleX는 0인 걸 알 수 있다.


색상이 R, G, B 채널 모두 같은 값인 회색 계열이기 때문에 나중에 componentX, componentY를 설정할 때 1, 2, 4 중 무엇을 써도 상관이 없다. 사각형을 일단 반절로 나눈 뒤 그래디언트 채우기를 썼다. ( 흰색(0xffffff) ~ 중간회색(0x808080) ) 가운데는 중간값이고 양 끝으로 갈 수록 중간값에서 멀어지기 때문에 V자 꼴로 꺾인다.


슬라이더의 값 범위는 -500 ~ 500 이고 DMF의 scaleY에 그대로 대입한다. (scaleY가 음수이면 중간값보다 클 경우 밑이 아니라 위쪽 픽셀의 색상을 가져온다)


라이브러리를 보면 logo 무비클립 심벌이 있는데 Logo 클래스로 내보내진다. 열어보면 이렇게 되어 있고 CODEONWORT라고 쓴 텍스트 필드는 무비 클립으로 감쌌는데 인스턴스 이름은 txt 이다.

코드 전문

대상 비트맵은 logoBD이고 맵 비트맵은 gradBD이다. 필터를 적용한 결과는 bd에 표시한다.

이번에는 DMF 설정을 전부 생성자에서 처리했다. 하나씩 설정하려면 이렇게 한다.

var dmf:DisplacementMapFilter = new DisplacementMapFilter()
dmf.mapBitmap = gradBD
dmf.mapPoint = zero
dmf.componentX = 1
dmf.componentY = 1
dmf.scaleX = 0
dmf.scaleY = 0
dmf.mode = "color"
dmf.color = 0xffffff
dmf.alpha = 1

밑의 세 개는 앞에서 설명하지 않았다. mode는 색상을 가져올 픽셀의 위치가 맵 비트맵의 밖인 경우를 처리할 방법이다.


맨 위에서 봤던 그림을 다시 보자. 밑의 분홍색은 어디서 온 픽셀인가? 바닥 부분을 처리하는 도중, 밑의 픽셀을 가져와야 하는데 밑에 더 이상 픽셀이 없어서 윗부분의 픽셀을 가져온 것이다. 공식에서 색상을 가져올 픽셀의 x, y좌표를 각각 맵의 가로, 세로 길이로 나머지 연산하여 그 값을 좌표로 쓰는 셈이다. 이게 mode = "wrap" (기본값) 으로 설정했을 때의 처리 방법이다. 처리 방법은 네 가지이며 flash.display.DisplacementMapFilterMode 클래스에 문자열 상수 대체용 속성들이 있다.


mode가 기본값인 "wrap" 이면 scaleY를 양수로 설정하여 글자가 V자로 꺾일 때 위치 변경 값이 범위를 벗어나서 반대쪽의 픽셀 색상을 가져오기 때문에 아래에 글자 일부가 보여버린다. 이 예제에선 반대쪽 색상을 가져오지만 않으면 되므로 "color"로 설정하고 color, alpha 속성은 건드리지도 않았는데 "clamp"로 설정해도 상관 없다.

완성 파일


2. 물결
라이브러리를 보면 크기 400x300인 수련.jpg가 Image라는 클래스로 내보내지게 설정되었고 프레임에는 이런 코드가 있다.

function update(e:Event=null):void {
// DMF 생성하고 적용하는 코드
}

/////////////////////////////////////////////////////////

update()
act_btn.addEventListener("click", toggleAct)
function toggleAct(e:Event):void {
act_btn.play()
if(act_btn.currentFrame == 1){
addEventListener(Event.ENTER_FRAME, update)
}else{
removeEventListener(Event.ENTER_FRAME, update)
}
}

////////// 밑은 플래시를 안 볼 때는 정지 버튼을 눌러 cpu 부하를 줄이기 위한 것이니 필요가 없고 update()만 완성하면 된다. 먼저 가로 방향으로 물결치게 해보자.

적용 전

적용 후

물결 모양으로 왜곡하면 그림이 커지기 때문에 대상 비트맵의 크기는 이미지의 크기보다 커야 한다. 이미지를 img, 진폭을 A라고 하면 양 옆으로 볼록한 부분이 있으니 대상 비트맵의 가로 길이가 적어도 이만큼이여야 왜곡한 결과를 모두 표시할 수 있다.

bd.width = img.width + A + A

그리고 이미지는 대상 비트맵의 가운데에 그려야 하므로

const A:Number = 30 // 진폭
const position:Matrix = new Matrix(1,0,0,1, A,A)
var img:BitmapData = new Image(400, 300)

var bd:BitmapData = new BitmapData(img.width+A+A, img.height+A+A, false, 0xffffff)
addChild(new Bitmap(bd))

// 그릴 때는 이렇게 한다 //
bd.draw(img, position)

코드는 이렇게 된다. 세로 왜곡의 진폭도 똑같이 A로 하고 bd의 높이를 미리 크게 해놓았다.

이제 DMF를 위한 맵 비트맵을 만들 것인데, 예제 0에서는 이미지를 그대로 맵 비트맵으로 썼고 예제 1에서는 플래시 IDE의 그래디언트 툴로 맵 비트맵을 직접 그렸지만 이번에는 코드로 픽셀을 일일이 칠한다. 색상은 0xRRGGBB 꼴로 입력하기 때문에 칠하기 쉽게 B 채널을 쓴다. 128을 중심으로 진폭이 127인 코사인파를 그릴 것이다.


한 주기만큼 구부리려면 x가 0에서 2π까지여야 하므로 x는 이렇게 된다.

// i = 0 -> x = 0
// i = map.height-1 -> x = 2π
for(i=0 ; i<map.height ; i++){
  x = 2π * i/(map.height-1)
}

그래서 코드는 이렇다.


사인파가 움직이도록 bias라는 항을 추가하고 매 프레임마다 조금씩 증가시킨다. map을 화면에 표시하면 수직 그래디언트로 보인다.


지금 실행하면 가로로만 물결친다.


가로 물결을 구현했으면 세로 물결을 추가하는 건 어렵지 않다.

코드 전문

완성 파일
* 사실 가로 왜곡을 할 땐 양 옆 30 픽셀에는 그릴 필요가 없다는 점을 이용하여 작업량을 줄일 수 있다. 하지만 가로 왜곡을 한 후에는 그림이 양 옆으로 늘어나므로 세로 왜곡을 할 때는 j가 0에서 map.width-1까지 가야 한다.
* map을 화면으로 보면 그래디언트가 밑으로 내려가고 윗부분만 새로 그린 것처럼 보인다. 최적화를 할 수 있을 것이다.
* 그래디언트 두 개를 그리고 붙여서 완전히 내려가면 다시 위로 올리는 식으로 모션 트위닝을 하면 코드로 칠하지 않아도 될 것이다.


3. 지니 이펙트
 
예제3과 4는 뼈대 FLA 같은 거 없고 완성 파일만 올립니다.

좀 조잡하지만 -_- 맥 OS X의 지니 이펙트를 따라해보자. for문으로 위에서 아래로 훑으며 각 줄을 압축한다는 개념으로 접근한다.

↕ 출처는 위키디피아 (http://en.wikipedia.org/wiki/Sigmoid_function)

위의 함수는 시그모이드 함수다. 플래시를 보면 압축 후의 경계 곡선이 그래프와 비슷하다는 걸 알 수 있다. 각 줄마다 적당한 t를 넣어 나온 함숫값을 압축 비율 삼아 가로로 압축하는 꼴이다. 예를 들어 어떤 줄에 해당하는 t를 넣어 함숫값이 0.3이 나왔다면 그 줄의 길이는 원래의 30%로 압축된다. 식에서 t의 범위는 -∞ ~ +∞ 이지만 -10 ~ 10으로 잡아도 함숫값이 대략 0에서 1까지 나오니까 이 범위를 쓴다.

하지만 시그모이드를 그대로 쓰면 왜곡이 항상 이렇게 되니


슬라이더 두 개로 최상단 줄의 압축 비율과 최하단 줄의 압축 비율을 조절한다. 두 슬라이더의 값 범위는 0~1 이다. 두 압축 비율을 순서대로 a, b라 하고

수식은 LaTeX로 입력

이렇게 수식을 바꾼다. 그런데 여기서 t의 범위는 [-10, 10]이고

for(var i:int=0 ; i<map.height ; i++){
// i번째 줄을 처리한다
}

이런 식으로 각 줄을 처리할 건데, i의 범위는 [0, map.height-1]이다. 그래서 [0, map.height-1] 안의 정수 i를 비율을 유지하면서 [-10, 10] 사이의 실수 t로 변환해야 한다. 다음 순서를 따른다.

1. i를 map.height-1로 나눈다. [0, map.height-1] -> [0, 1]
2. 0.5를 뺀다. [0, 1] -> [-0.5, 0.5]
3. 10을 곱한다. [-10, 10]

코드 전문


4. 시각화 : 광기
 
사실 이번 맵 비트맵은 수식으로 설명할 수가 없다. 예제는 대충 이런 순서로 작동한다.

- Embed 메타데이터 태그로 같은 폴더 내의 music.mp3를 swf에 끼워넣는다.
- BitmapData 객체인 bd를 준비한다.
- enterFrame 수신자에서
  - 지금 음량에 반지름이 비례하게 sh에 원을 그린다.
    화면 중심에서 조금씩 벗어나고 조금 투명하게 그린다. 색깔은 점점 변하게 만든다.
  - sh를 bd에 그린다.
  - bd에 흐림 필터를 적용한다.
  - bd에 DMF를 적용한다. 대상도 맵도 bd다.

원래 그림과 왜곡 후 그림이 부드럽게 이어지는 이유는 이웃한 픽셀들의 색상 차이가 적어서 중앙값과의 차이가 조금씩 차이나기 때문에 왜곡도 조금씩 되기 때문이다.

코드 전문
11 Comments
댓글쓰기 폼