5. 커스텀 필터

손을 더럽힐 준비가 되셨습니까? 우리는 이제 간단한 조각 필터로 시작하는 사용자 지정 렌더링 코드의 영역에 들어서고 있습니다.

네. 여기에는 저수준 코드가 포함될 것입니다. 도대체 몇 줄의 어셈블러를 작성할 것입니다! 그러나 두려워할 일은 아닙니다. 로켓 과학이 아니니까요. 나의 예전 수학 선생님 왈: 멍청한 원숭이는 그것을 할 수 있다!

기억하세요: 필터는 표시 객체의 픽셀 수준에서 작동합니다. 필터링된 객체는 텍스처로 렌더링된 다음 사용자 정의 조각 쉐이더 (따라서 이름 조각 필터)로 처리됩니다.

5.1. 목표

우리가 단순한 목표를 설정했지만 유용한 것이어야 합니다. 그렇죠? 자, 이제 ColorOffsetFilter를 만듭시다.

색상을 지정하여 메쉬의 정점에 색을 입힐 수 있다는 것을 알고 있을 것입니다. 렌더링시 색상에 텍스처 색상이 곱해지므로 텍스처의 색상을 매우 간단하고 신속하게 수정할 수 있습니다:

var image:Image = new Image(texture);
image.color = 0x808080; // R = G = B = 0.5

수학적 배경 지식은 매우 간단합니다: GPU에서 각 색상 채널 (빨강 초록 파랑)은 0과 1 사이의 값으로 표시됩니다. 예를 들어 순수한 빨강이 될 것입니다:

R = 1, G = 0, B = 0

렌더링시 이 색상은 텍스처의 각 픽셀의 색상( “texel, 텍셀”이라고도 함)과 곱해집니다. 이미지 색상의 기본값은 모든 채널에서 1인 순수한 흰색입니다. 따라서 텍셀 컬러는 변경되지 않은 것처럼 보입니다 (1과 곱셈은 무연산입니다). 다른 색상을 지정하면 곱셈은 새로운 색상을 생성합니다:

R = 1,   G = 0.8, B = 0.6  ×
B = 0.5, G = 0.5, B = 0.5
-------------------------
R = 0.5, G = 0.4, B = 0.3

그리고 여기에 문제가 있습니다: 이것은 오직 대상을 더 어둡게 결코 더 밝게 만들지 않을 것입니다. 이는 0과 1 사이의 값만 곱할 수 있기 때문입니다. 0은 결과가 검은 색이고 하나는 의미가 없음을 의미합니다.

Tinting
그림 49. 이미지를 회색으로 착색.

이것이 우리가 이 필터로 해결하고자 하는 것입니다! 이 공식에 오프셋을 포함할 것입니다. 클래식 플래시에서는 ColorTransform을 사용하면 됩니다.

  • 새로운 red 값 = (기존 red 값 × redMultiplier) + redOffset
  • 새로운 green 값 = (기존 green 값 × greenMultiplier) + greenOffset
  • 새로운 blue 값 = (기존 blue 값 × blueMultiplier) + blueOffset
  • 새로운 alpha 값 = (기존 alpha 값 × alphaMultiplier) + alphaOffset

승수(multiplier)는 이미 기본 Mesh 클래스에서 처리되므로 이미 배수가 있습니다. 필터는 오프셋을 추가해야 합니다.

Offset
그림 50. 모든 채널에 offset 더하기.

드디어 시작합시다, 함께 하실까요?!

5.2. FragmentFilter 확장

모든 필터는 starling.filters.FragmentFilter 클래스를 확장하며 이 필터도 예외는 아닙니다. 이제 꽉 잡으세요. 이제 완전한 ColorOffsetFilter 클래스를 제공하겠습니다. 이것은 스텁이 아니라 최종 코드입니다. 더 이상 수정하지 않겠습니다.

public class ColorOffsetFilter extends FragmentFilter
{
    public function ColorOffsetFilter(
        redOffset:Number=0, greenOffset:Number=0,
        blueOffset:Number=0, alphaOffset:Number=0):void
    {
        colorOffsetEffect.redOffset = redOffset;
        colorOffsetEffect.greenOffset = greenOffset;
        colorOffsetEffect.blueOffset = blueOffset;
        colorOffsetEffect.alphaOffset = alphaOffset;
    }

    override protected function createEffect():FilterEffect
    {
        return new ColorOffsetEffect();
    }

    private function get colorOffsetEffect():ColorOffsetEffect
    {
        return effect as ColorOffsetEffect;
    }

    public function get redOffset():Number
    {
        return colorOffsetEffect.redOffset;
    }

    public function set redOffset(value:Number):void
    {
        colorOffsetEffect.redOffset = value;
        setRequiresRedraw();
    }

    // 그에 따라 다른 오프셋 속성을 구현해야 합니다.

    public function get/set greenOffset():Number;
    public function get/set blueOffset():Number;
    public function get/set alphaOffset():Number;
}

그것은 놀랍도록 컴팩트한 것입니다. 그렇죠? 글쎄..나는 인정해야만 합니다: 이것은 이야기의 절반에 불과한데요. 실제 색상 처리를 하는 또 다른 클래스도 써야하기 때문입니다. 그래도 우리가 위에서 본 것을 분석하는 것은 가치가 있습니다.

물론 클래스는 FragmentFilter를 확장하고 createEffect 메서드를 재정의합니다. starling.rendering.Effect 클래스는 예전에 저수준 렌더링을 위해서만 필요했기 때문에 사용하지 않았을 것입니다. API 문서의 내용:

이펙트는 Stage3D 그리기 작업의 모든 단계를 캡슐화합니다. 렌더링 컨텍스트를 구성하고 쉐이더 프로그램과 인덱스 및 버텍스 버퍼를 설정하여 모든 하위 레벨 렌더링의 기본 메커니즘을 제공합니다.

FragmentFilter 클래스는 이 클래스 또는 실제로 FilterEffect라는 하위 클래스를 사용합니다. 이 간단한 필터의 경우 createEffect()를 재정의하여 사용자 지정 효과를 제공하면 됩니다. 속성은 우리의 효과를 구성하는 것 외에는 아무것도 하지 않습니다. 렌더링 시 기본 클래스는 필터를 렌더링 하는데 자동으로 이 효과를 사용합니다. 그게 다입니다!

colorOffsetEffect 속성이 하는 일이 궁금하다면 ColorOffsetEffect에 지속적으로 캐스트하지 않고 효과에 액세스 할 수 있는 바로 가기입니다. 기본 클래스는 효과 속성도 제공하지만 이는 FilterEffect 유형의 객체를 반환하며 offset 속성에 액세스하려면 전체 유형인 ColorOffsetEffect가 필요합니다.

보다 복잡한 필터는 프로세스 메서드를 재정의 해야할 수도 있습니다. 예를들어, 멀티 패스 필터를 만드는 데 필요합니다. 샘플 필터의 경우에는 필요하지 않습니다.

마지막으로 setRequiresRedraw에 대한 호출을 주목하십시오. 설정이 변경될 때마다 효과가 다시 렌더링되는지 확인하십시오. 그렇지 않으면 Starling은 개체를 다시 그려야한다는 것을 알 수 없습니다.

5.3. FilterEffect 확장

실제 작업을 할 시간입니다. 그렇죠? 음, 우리의 FilterEffect 서브 클래스는 이 필터의 실제 주력자입니다. 그렇다고 이것이 매우 복잡하다는 것을 의미하지 않기 때문에 나와 함께 감내해 보시죠.

스텁(stub)으로 시작하시죠:

public class ColorOffsetEffect extends FilterEffect
{
    private var _offsets:Vector.<Number>;

    public function ColorOffsetEffect()
    {
        _offsets = new Vector.<Number>(4, true);
    }

    override protected function createProgram():Program
    {
        // TODO
    }

    override protected function beforeDraw(context:Context3D):void
    {
        // TODO
    }

    public function get redOffset():Number { return _offsets[0]; }
    public function set redOffset(value:Number):void { _offsets[0] = value; }

    public function get greenOffset():Number { return _offsets[1]; }
    public function set greenOffset(value:Number):void { _offsets[1] = value; }

    public function get blueOffset():Number { return _offsets[2]; }
    public function set blueOffset(value:Number):void { _offsets[2] = value; }

    public function get alphaOffset():Number { return _offsets[3]; }
    public function set alphaOffset(value:Number):void { _offsets[3] = value; }
}

우리는 벡터에 오프셋을 저장하고 있습니다. 오프셋을 GPU에 쉽게 업로드 할 수 있기 때문입니다. 해당 벡터에서 읽고 쓰는 오프셋 속성입니다. 충분히 간단합니다.

재정의된 두 가지 방법을 살펴보면 더 흥미로워집니다.

createProgram

이 메서드는 실제 Stage3D 셰이더 코드를 생성합니다.

기본을 보여 드리겠지만 Stage3D에 대해 자세히 설명하는 것은 이 설명서의 범위를 벗어납니다. 주제에 대해 자세히 알아보려면 항상 다음 자습서 중 하나를 살펴보십시오:

모든 Stage3D 렌더링은 정점 및 조각 쉐이더를 통해 수행됩니다. 그것들은 GPU에 의해 직접 실행되는 작은 프로그램이며 두 가지 종류가 있습니다:

  • Vertex Shaders 는 각 꼭지점에 대해 한 번 실행됩니다. 입력은 VertexData 클래스를 통해 일반적으로 설정하는 정점 속성으로 구성됩니다. 그들의 출력은 화면 좌표의 정점 위치입니다.
  • Fragment Shaders 는 각 픽셀 (조각)에 대해 한 번 실행됩니다. 입력은 삼각형의 세 꼭지점의 보간 속성으로 구성됩니다. 출력은 단순히 픽셀의 색상입니다.
  • 조각과 정점 셰이더가 함께 *프로그램*을 구성합니다.

언어 필터(language filters)는 어셈블리 언어인 AGAL로 작성됩니다. (네. 당신이 읽을 수 있습니다! 이것은 낮은 수준입니다.) 고맙게도 전형적인 AGAL 프로그램은 매우 짧기 때문에 나쁘지는 않습니다.

좋은 소식은 조각 쉐이더만 작성하면 됩니다. 버텍스 쉐이더는 대부분의 프래그먼트 필터에서 동일하므로 Starling은 이를 위한 표준 구현을 제공합니다. 코드를 살펴 보겠습니다:

override protected function createProgram():Program
{
    var vertexShader:String = STD_VERTEX_SHADER;
    var fragmentShader:String =
        "tex ft0, v0, fs0 <2d, linear> \n" +
        "add oc, ft0, fc0";

    return Program.fromSource(vertexShader, fragmentShader);
}

약속대로 버텍스 쉐이더는 상수로부터 가져옵니다. 프래그먼트 셰이더는 두 줄의 코드일 뿐입니다. 둘 다 메소드의 리턴 값인 하나의 프로그램 인스턴스로 결합됩니다.

조각 쉐이더는 물론 좀 더 정교함이 필요합니다.

Nutshell 내의 AGAL

AGAL에서 각 행에는 간단한 메소드 호출이 들어 있습니다.

[opcode] [destination], [argument 1], ([argument 2])
  • 처음 세 문자는 작업의 이름입니다 (tex, add).
  • 다음 인수는 조작 결과가 저장되는 위치를 정의합니다.
  • 다른 인수는 메소드의 실제 인수입니다.
  • 모든 데이터는 사전 정의된 레지스터에 저장됩니다. 그것들을 Vector3D 인스턴스 (x, y, z 및 w에 대한 속성 포함)로 생각하십시오.

여러 가지 유형의 레지스터가 있습니다 (예: 상수 임시 데이터 또는 셰이더 출력용). 셰이더에서는 일부 데이터에 이미 데이터가 포함되어 있습니다. 그들은 필터의 다른 방법으로 설정되었습니다. (나중에 설명하겠습니다)

  • v0 에는 현재 텍스처 좌표가 포함됩니다 (레지스터 0 변경).
  • fs0 은 입력 텍스처를 가리킴 (프래그먼트 샘플러 0)
  • fc0 에는 이것에 대한 색 오프셋이 포함되어 있습니다 (조각 상수 0).

프래그먼트 셰이더의 결과는 항상 색상이어야 합니다. 그 색상은 oc 레지스터에 저장됩니다.

코드 리뷰

우리의 프래그먼트 셰이더의 실제 코드로 돌아가 봅시다. 첫 번째 줄은 텍스처에서 색상을 읽습니다:

tex ft0, v0, fs0 <2d, linear>

레지스터 v0에서 읽은 텍스처 좌표와 일부 옵션 (2d linear)으로 텍스처 fs0을 읽습니다. 텍스처 좌표가 v0에있는 이유는 표준 정점 셰이더 (STD_VERTEX_SHADER)가 거기에 저장하기 때문입니다; 믿어주세요. 결과는 임시 레지스터 ft0에 저장됩니다 (AGAL에서는 결과가 항상 연산의 첫 번째 인수에 저장됩니다).

잠깐만요. 우리는 어떤 텍스쳐도 만들어 내지 못했습니다. 그렇죠? 이게 뭐죠?

위에서 쓴 것처럼 조각 필터는 픽셀 단위로 작동합니다. 그것의 입력은 텍스쳐로 렌더링된 원래의 객체입니다. 우리의 기본 클래스 (FilterEffect)는 우리를 위해 그것을 설정합니다; 프로그램이 실행되면 텍스처 샘플러 fs0이 필터링되는 객체의 픽셀을 가리키는지 확인할 수 있습니다.

사실 이 줄을 조금 바꾸고 싶습니다. 마지막에 텍스처 데이터를 해석하는 방법을 나타내는 옵션을 발견했을 것입니다. 글쎄요. 이 옵션은 우리가 접근하고 있는 텍스처 유형에 달려 있다고 밝혀졌습니다. 코드가 모든 텍스처에 대해 작동하는지 확인하려면 도우미 메서드를 사용하여 해당 AGAL 연산을 작성하십시오.

tex("ft0", "v0", 0, this.texture)

그것은 똑같이 (AGAL 문자열을 반환하는) 방법이지만 더 이상 옵션을 신경 쓸 필요가 없습니다. 텍스처에 액세스 할 때는 항상이 메서드를 사용하십시오. 여러분이 밤에 잘 잘 수 있게 해줄 것입니다.

두 번째 라인은 실제로 우리가 여기 온 작업을 수행합니다. 텍셀 색상에 색상 오프셋을 추가합니다. 오프셋은 fc0에 저장되며 곧 살펴볼 것입니다. 그것은 ft0 레지스터 (방금 읽은 텍셀 색상)에 추가되고 출력 레지스터 (oc)에 저장됩니다.

add oc, ft0, fc0

지금은 AGAL과 같습니다. 오버라이드된 다른 메소드를 살펴 보겠습니다.

beforeDraw

beforeDraw 메서드는 셰이더가 실행되기 전에 직접 실행됩니다. 셰이더에 필요한 모든 데이터를 설정할 때 사용할 수 있습니다.

override protected function beforeDraw(context:Context3D):void
{
    context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, _offsets);
    super.beforeDraw(context);
}

이것은 프래그먼트 셰이더에 오프셋 값을 전달하는 곳입니다. 두 번째 매개 변수 인 0은 데이터가 끝날 레지스터를 정의합니다. 실제 쉐이더 코드를 살펴보면 fc0에서 오프셋을 읽음을 알 수 있습니다. 여기 정확히 채우면 됩니다. 프래그먼트 상수 0.

슈퍼 콜은 모든 나머지를 설정합니다. 텍스처 (fs0)와 텍스처 좌표를 할당합니다.

여러분이 질문하기 전에: 네. afterDraw() 메서드도 있습니다. 일반적으로 리소스를 정리하는 데 사용됩니다. 그러나 상수의 경우에는 필요하지 않으므로 이 필터에서는 무시해도 됩니다.

5.4. 시도하기

우리 필터가 실제로 준비되었습니다. 여기에서 전체 코드를 다운로드하십시오! 시운전 시간입니다.

var image:Image = new Image(texture);
var filter:ColorOffsetFilter = new ColorOffsetFilter();
filter.redOffset = 0.5;
image.filter = filter;
addChild(image);

Custom Filter PMA Issue
그림 51. 이 필터에는 부작용이 있는 것 같습니다.

이럴수가! 네. 빨간색 값이 확실히 높긴 하지만 왜 지금 새의 영역을 넘어서 확장하고 있을까요? 우리는 결국 알파 값을 변경하지 않았습니다!

당황하지 마십시오. 방금 첫 번째 필터를 만들었고 그것은 여러분에게 동기부여가 되지 않았나요? 그것은 가치가 있어야 합니다. 할 수 있는 미세 조정이 있을 것으로 예상됩니다.

“premultiplied alpha, 미리 곱셈 된 알파”(PMA)를 사용하는 것을 잊어 버렸네요. 모든 일반적인 텍스처는 RGB 채널에 알파 값이 미리 곱하여 저장됩니다. 아래처럼 50% 알파를 가진 빨강 말이죠:

R = 1, G = 0, B = 0, A = 0.5

실제로 이런 식으로 저장됩니다:

R = 0.5, G = 0, B = 0, A = 0.5

그리고 우리는 이것을 고려하지 않았습니다. 그가 해야할 일은 오프셋 값을 출력에 추가하기 전에 현재 픽셀의 알파 값으로 곱하는 것입니다. 이를 수행하는 한 가지 방법이 있습니다:

tex("ft0", "v0", 0, texture)   // 텍스처에서 색상 가져 오기
mov ft1, fc0                   // 전체 오프셋을 ft1에 복사합니다.
mul ft1.xyz, fc0.xyz, ft0.www  // alpha (pma!)로 offset.rgb를 곱하십시오.
add  oc, ft0, ft1              // 오프셋 추가, 출력에 복사

보시다시피 레지스터의 xyzw 속성에 액세스하여 개별 색상 채널 (rgba 채널에 해당)에 액세스 할 수 있습니다.

텍스처가 PMA와 함께 저장되지 않으면 어떻게 될까요? tex 메서드는 항상 PMA로 값을 수신하므로 걱정할 필요가 없습니다.

두 번째 시도

필터를 다른 것으로 시도하면 (완전한 코드: ColorOffsetFilter.as) 올바른 알파 값을 볼 수 있습니다:

Custom Filter with solved PMA issue
그림 52. 더 좋아요!

축하합니다! 방금 첫 번째 필터를 만들었고 완벽하게 작동합니다. (그렇습니다, 당신은 Starling의 ColorMatrixFilter를 대신 사용할 수있었습니다 -하지만 이게 조금 더 빠르므로 노력할 만한 가치가 있었습니다.)

좀 더 용기가 필요하지만, 이제 그 대신에 메쉬 스타일로 같은 결과를 얻을 수 있으니 시도해 보세요. 그다지 다르지는 않아요. 약속하죠!

Was this article helpful?

Related Articles