3.6. 커스텀 스타일

이제 Stage3D의 파워를 활용했으므로 이 길을 계속가십시오! 이 섹션에서는 간단한 메쉬 스타일을 작성합니다. Starling 2에서는 모든 렌더링이 스타일을 통해 수행됩니다. 자신만의 스타일을 만들어 어떤 식으로든 성능을 희생하지 않고 특수 효과를 만들 수 있습니다.

계속하기 전에 ‘커스텀 필터, Custom Filters’ 섹션을 읽었는지 확인하십시오. 필터와 스타일은 많은 개념을 공유하므로 두 가지 중 더 간단한 것으로 시작하는 것이 좋습니다. 아래에서는 다른 섹션에 표시된 모든 내용을 잘 알고 있다고 가정합니다.

3.6.1. 목표

목표는 ColorOffsetFilter와 같습니다. 모든 렌더링된 픽셀의 색상 값에 오프셋을 추가할 수 있게 하는 것입니다. 이번에 우리는 그것을 스타일로 합니다! 우리는 그것을 ColorOffsetStyle이라고 부를 것입니다.

그림 53. 스타일을 적용한 색상 오프셋 적용.

계속하기 전에 필터와 스타일의 차이점을 이해하는 것이 중요합니다.

필터 vs. 스타일

앞서 언급했듯이 필터는 픽셀 단위로 작동합니다. 객체는 텍스처로 렌더링되고 필터는 어떤 방식으로든 해당 텍스처를 처리합니다. 반면 스타일은 객체의 모든 원래 기하학에 액세스하거나 더 정확하게는 객체의 정점에 액세스할 수 있습니다.

이렇게 하면 스타일이 일부 제한됩니다 (예 : 스타일로 흐림 효과를 얻을 수 없음). 첫 번째로, 객체를 텍스처로 그리는 첫 번째 단계가 필요 없기 때문입니다. 둘째, 가장 중요한 것은 스타일이 지정된 메시를 일괄 처리 할 수 ​​있습니다.

아시다시피, 드로우 콜의 수를 줄이는 것은 높은 프레임 속도에 매우 중요합니다. 이러한 상황이 발생하는지 확인하기 위해 Starling은 드로잉을 하기 전에 최대한 많은 오브젝트를 배치합니다. 문제는 함께 묶을 수있는 객체를 결정하는 방법입니다. 스타일이 작동하는 위치: 같은 스타일의 오브젝트만 함께 배치할 수 있습니다.

스테이지에 ColorOffsetFilter가 적용된 세 개의 이미지를 추가하면 적어도 세 번의 드로우 콜이 표시됩니다. 대신에 ColorOffsetStyle을 사용하여 세 개의 객체를 추가하면 하나만 있을 것입니다. 따라서 스타일을 작성하기가 조금 더 어려워집니다. 하지만 그만큼 가치가 있습니다!

3.6.2. MeshStyle 확장

모든 스타일의 기본 클래스는 starling.styles.MeshStyle입니다. 이 클래스는 우리가 필요로 하는 모든 인프라를 제공합니다. 먼저 스텁을 살펴 보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class ColorOffsetStyle extends MeshStyle
{
    public static const VERTEX_FORMAT:VertexDataFormat =
            MeshStyle.VERTEX_FORMAT.extend("offset:float4");

    private var _offsets:Vector.<Number>;

    public function ColorOffsetStyle(
        redOffset:Number=0, greenOffset:Number=0,
        blueOffset:Number=0, alphaOffset:Number=0):void
    {
        _offsets = new Vector.<Number>(4, true);
        setTo(redOffset, greenOffset, blueOffset, alphaOffset);
    }

    public function setTo(
        redOffset:Number=0, greenOffset:Number=0,
        blueOffset:Number=0, alphaOffset:Number=0):void
    {
        _offsets[0] = redOffset;
        _offsets[1] = greenOffset;
        _offsets[2] = blueOffset;
        _offsets[3] = alphaOffset;

        updateVertices();
    }

    override public function copyFrom(meshStyle:MeshStyle):void
    {
        // TODO
    }

    override public function createEffect():MeshEffect
    {
        return new ColorOffsetEffect();
    }

    override protected function onTargetAssigned(target:Mesh):void
    {
        updateVertices();
    }

    override public function get vertexFormat():VertexDataFormat
    {
        return VERTEX_FORMAT;
    }

    private function updateVertices():void
    {
        // TODO
    }

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

    // the other offset properties need to be implemented accordingly.

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

그것이 우리의 출발점입니다. 마지막 예제의 초기 필터 클래스보다 조금 더 많은 부분이 이미 있음을 알 수 있습니다. 이제 코드의 개별 부분을 살펴 보겠습니다.

버텍스 포맷(Vertex Formats)

가장 주목할만한 점은 클래스 맨 꼭대기에 있는 정점 형식 상수입니다. 이미 스타일은 오브젝트의 모든 지오메트리에 대한 액세스를 제공하는 정점 레벨에서 작동한다고 언급했습니다. VertexData 클래스는 그 지오메트리를 저장하지만, 이 클래스가 자신에게 저장되는 데이터와 해당 클래스를 어떻게 알 수 있는지 결코 설명하지 않았습니다. 이것은 VertexDataFormat에 의해 정의됩니다.

MeshStyle에서 사용하는 기본 형식은 다음과 같습니다.


1
position:float2, texCoords:float2, color:bytes4

이 문자열의 구문은 익숙해 보일 것입니다. 특정 데이터 유형이 있는 속성 목록입니다.

  • position 속성은 두 개의 부동 소수점을 저장합니다 (정점의 x 및 y 좌표 용).

  • texCoords 속성은 두 개의 부동 소수점을 저장합니다 (꼭지점의 텍스처 좌표).

  • color 속성은 정점 색상 (각 채널 당 1 바이트)에 대해 4 바이트를 저장합니다.

이 형식의 VertexData 인스턴스는, 서식 캐릭터 라인과 완전히 같은 순서로, 메시의 각 정점에 대해 그러한 속성을 포함합니다. 이것은 각 버텍스가 20 바이트 (8 + 8 + 4)를 차지한다는 것을 의미합니다.

메쉬를 생성하고 특히 스타일을 지정하지 않으면 표준 MeshStyle에 의해 렌더링되어 정확하게 이 형식을 정점에 강제 적용합니다. 이것은 결국 질감이 있는 채색된 메쉬를 그리는 데 필요한 모든 정보입니다.

하지만 ColorOffsetStyle의 경우 충분하지 않습니다. 색상 오프셋도 저장해야 합니다. 따라서 4 개의 부동 소수점 값으로 구성된 오프셋 속성을 추가하는 새로운 형식을 정의해야 합니다.


1
2
MeshStyle.VERTEX_FORMAT.extend("offset:float4");
// => position:float2, texCoords:float2, color:bytes4, offset:float4

자, 여러분은 이제 질문할 것입니다. 왜 우리는 이것을 필요로 합니까? 결국 이 필터는 맞춤 버텍스 형식을 사용하지 않고도 정상적으로 작동했습니다.

그건 아주 좋은 질문입니다, 당신이 물어봐서 기뻐요! 그 답은 Starling의 일괄 처리 코드에 있습니다. 우리가 스타일을 일부 후속 메쉬에 지정할 때, 그것들은 함께 일괄적으로 배치될 것입니다. 이것이 바로 우리가 이 노력을 하는 이유입니다.

하지만 배치는 무엇을 의미할까요? 그것은 단지 모든 개별 메쉬의 정점을 하나의 더 큰 메쉬로 복사하여 렌더링한다는 것을 의미합니다. Starling의 렌더링 내부에는 다음과 비슷한 코드가 있습니다.


1
2
3
4
5
6
7
8
var batch:Mesh = new Mesh();

batch.add(meshA);
batch.add(meshB);
batch.add(meshC);

batch.style = meshA.style; // ← !!!
batch.render();

문제가 보이나요? 큰 메쉬 (일괄 처리)는 처음 추가된 메쉬 스타일의 복사본을 받습니다. 하지만, 이 세 가지 스타일은 아마도 다른 설정을 사용할 것입니다. 이러한 설정이 스타일에 저장되는 경우 렌더링을 통해 하나만 제외하고 모두 사라집니다. 대신 스타일은 대상 메쉬의 VertexData에 데이터를 저장해야 합니다! 그래야 큰 배치 메쉬가 모든 오프셋을 개별적으로 받을 수 있습니다.

그것이 중요하기 때문에 나는 다음과 같이 다시 한번 말합니다: 스타일의 설정은 항상 대상 메쉬의 정점 데이터에 저장해야 합니다.

관습에 따라 정점 형식은 항상 스타일 클래스의 정적 상수로 액세스 할 수 있으며 vertexFormat 속성에서도 반환됩니다. 스타일이 메시에 지정되면 정점이 자동으로 새 형식에 맞춰집니다.

당신이 그 개념을 이해할 때, 당신은 이미 이 모든 과정의 중간 단계일 것입니다. 나머지는 단편 상수 대신 정점 데이터에서 오프셋을 읽도록 코드를 업데이트하는 것입니다.

하지만 나는 나 자신보다 앞서 가고 있습니다.

멤버 변수(Member Variables)

방금 모든 데이터가 꼭지점에 저장되어야 한다고 주장했지만 여전히 멤버 변수에 저장된 오프셋 집합이 있습니다.


1
private var _offsets:Vector.<Number>;

개발자가 스타일을 메시에 지정하기 전에 구성할 수 있기를 바랍니다. 대상 객체가 없으면 이러한 오프셋을 저장할 수 있는 정점 데이터가 없습니다. 그래서 우리는 이 벡터를 대신 사용할 것입니다. 대상이 지정되자마자 값은 대상의 정점 데이터에 복사됩니다 (onTargetAssigned 참조).

copyFrom

일괄 처리 중 스타일은 가끔씩 한 인스턴스에서 다른 인스턴스로 복사되어야 합니다 (주로 가비지 컬렉터를 고민하지 않고도 스타일을 다시 사용할 수 있어야 함). 따라서, copyFrom 메소드를 오버라이드 (override) 할 필요가 있습니다. 우리는 이렇게 할 것입니다 :


1
2
3
4
5
6
7
8
9
10
11
override public function copyFrom(meshStyle:MeshStyle):void
{
    var colorOffsetStyle:ColorOffsetStyle = meshStyle as ColorOffsetStyle;
    if (colorOffsetStyle)
    {
        for (var i:int=0; i<4; ++i)
            _offsets[i] = colorOffsetStyle._offsets[i];
    }

    super.copyFrom(meshStyle);
}

이것은 다소 간단합니다. 복사하려는 스타일이 올바른 유형인지 확인한 다음 현재 인스턴스에서 모든 오프셋을 복제합니다. 나머지는 수퍼 클래스가 수행합니다.

createEffect

이건 좀 친근해 보이네요. 그렇죠?


1
2
3
4
override public function createEffect():MeshEffect
{
    return new ColorOffsetEffect();
}

그것은 필터 클래스처럼 작동합니다; 나중에 생성할 ColorOffsetEffect를 반환할 것입니다. 아니요. 오프셋 값이 정점에서 읽혀지기 때문에 필터에 사용 된 것과 같지 않지만 두 가지 모두에 적용되는 효과를 만들 수 있습니다.

onTargetAssigned

위에서 언급했듯이 타겟 메쉬의 정점 데이터에 오프셋을 저장해야 합니다. 예, 이것은 각 옵셋이 모든 버텍스에 저장된다는 것을 의미합니다. 스타일이 일괄 처리를 지원한다는 것을 보장하는 유일한 방법입니다.

필터에 대상이 지정되면 이 콜백이 실행됩니다. 즉, 정점을 업데이트하는 단서가 됩니다. 우리는 다른 곳에서도 이 작업을 다시 수행할 것이므로 실제 프로세스를 updateVertices 메소드로 옮겼습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override protected function onTargetAssigned(target:Mesh):void
{
    updateVertices();
}

private function updateVertices():void
{
    if (target)
    {
        var numVertices:int = vertexData.numVertices;
        for (var i:int=0; i<numVertices; ++i)
            vertexData.setPoint4D(i, "offset",
                _offsets[0], _offsets[1], _offsets[2], _offsets[3]);

        setRequiresRedraw();
    }
}

해당 vertexData 객체의 출처를 궁금해 할 수 있습니다. 대상이 지정되자 마자 vertexData 속성은 대상의 꼭짓점을 참조합니다 (스타일 자체는 결코 어떤 꼭짓점도 소유하지 않습니다). 따라서 위의 코드는 대상 메쉬의 모든 정점을 반복하고 올바른 오프셋 값을 할당하므로 렌더링 중에 사용할 준비가 되었습니다.

3.6.3. MeshEffect 확장

이제 스타일 클래스가 완성되었습니다. 실제 렌더링이 이루어지는 곳으로 이동합니다. 이번에는 MeshEffect 클래스를 확장할 것입니다. 효과는 저수준 렌더링 코드 작성을 단순화 함을 기억하십시오. 저는 실제로 다음과 같은 상속을 가진 클래스 그룹에 대해 말하고 있습니다 :

기본 클래스 (효과)는 절대 최솟값만 수행합니다. 흰색 삼각형을 그립니다. FilterEffect는 텍스처에 대한 지원을 추가하고 색상 및 알파에 대한 MeshEffect를 추가합니다.

이 두 클래스는 TexturedEffect와 ColoredTexturedEffect라는 이름이 붙어있을 수도 있지만 사용법을 염두에두고 이름을 지정했습니다. 필터를 만들면 FilterEffect를 확장해야 합니다. 메쉬 스타일, MeshEffect를 만든다면.

이제 ColorOffsetEffect의 설정을 보겠습니다. 몇 개의 스텁이 나중에 채워집니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ColorOffsetEffect extends MeshEffect
{
    public  static const VERTEX_FORMAT:VertexDataFormat =
        ColorOffsetStyle.VERTEX_FORMAT;

    public function ColorOffsetEffect()
    { }

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

    override public function get vertexFormat():VertexDataFormat
    {
        return VERTEX_FORMAT;
    }

    override protected function beforeDraw(context:Context3D):void
    {
        super.beforeDraw(context);
        vertexFormat.setVertexBufferAt(3, vertexBuffer, "offset");
    }

    override protected function afterDraw(context:Context3D):void
    {
        context.setVertexBufferAt(3, null);
        super.afterDraw(context);
    }
}

이전 자습서의 아날로그 필터 효과와 비교하면 모든 오프셋 속성이 제거 된 것을 볼 수 있습니다. 이제 우리는 이제 vertexFormat을 오버라이드하여 해당 스타일과 동일한 형식을 사용하고 오프셋 값을 각 꼭지점에 저장할 준비가 되었습니다.

beforeDraw와 afterDraw

이제 beforeDraw 및 afterDraw 메소드는 셰이더의 오프셋 특성을 va3 (정점 특성, vertex attribute 3)으로 읽을 수 있도록 컨텍스트를 구성합니다. beforeDraw에서 그 라인을 살펴 보겠습니다.


1
vertexFormat.setVertexBufferAt(3, vertexBuffer, "offset");

이것은 다음과 같습니다.


1
context.setVertexBufferAt(3, vertexBuffer, 5, "float4");

세 번째 매개 변수 (5 → bufferOffset)는 꼭지점 형식 내에서 색상 오프셋의 위치를 나타내고 마지막 점은 속성의 형식을 나타냅니다 (float4 → format). 이러한 값을 계산하고 기억할 필요가 없도록 vertexFormat 객체에 해당 속성을 설정하도록 요청할 수 있습니다. 그렇게 했을 때 형식이 변경되면 코드가 계속 작동하며 오프셋 전에 다른 속성을 추가합니다.

드로우 콜은 다른 형식을 사용하기 때문에, 드로잉이 끝나면 버텍스 버퍼 속성은 항상 지워져야 합니다. 그게 우리가 afterDraw 메소드에서 하는 일입니다.

createProgram

마침내 스타일의 핵심을 다룰 시간입니다. 실제 렌더링을 수행하는 AGAL 코드 이번에는 버텍스 쉐이더도 구현해야 합니다. 사용자 정의 로직을 추가해야 하기 때문에 표준 구현을 사용하지 않습니다. 그러나 조각 쉐이더는 필터에 대해 작성한 셰이더 쉐이더와 거의 동일합니다. 한 번 보시죠!


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
override protected function createProgram():Program
{
    var vertexShader:String = [
        "m44 op, va0, vc0", // 출력 클립 공간에 대한 4x4 행렬 변환
        "mov v0, va1     ", // 프래그먼트 프로그램에 텍스처 좌표 전달
        "mul v1, va2, vc4", // 알파 (vc4)에 color (va2)를 곱하고, fp로 전달합니다.
        "mov v2, va3     "  // fp로 오프셋 전달
    ].join("\n");

    var fragmentShader:String = [
        tex("ft0", "v0", 0, texture) +  // 텍스처에서 색상 가져 오기
        "mul ft0, ft0, v1",             // 텍셀 색상으로 색상을 곱하십시오.
        "mov ft1, v2",                  // ft1에 전체 오프셋 복사
        "mul ft1.xyz, v2.xyz, ft0.www", // alpha (pma!)로 offset.rgb를 곱하십시오.
        "add oc, ft0, ft1"              // 오프셋을 추가하고 출력에 복사
    ].join("\n");

    return Program.fromSource(vertexShader, fragmentShader);
}

버텍스 쉐이더가 무엇을 하는지 이해하려면 먼저 작업하고 있는 입력을 이해해야 합니다.

  • VA 레지스터 ( 「정점 속성」)에는, 정점 버퍼로부터 취해진 현재 정점의 속성이 포함됩니다. 그것들은 조금 더 일찍 설정한 정점 포맷의 속성과 같이 정렬됩니다. va0은 정점 위치이고, va1은 텍스처 좌표이며, va2는 색상이며, va3는 오프셋입니다.

  • 두 개의 상수는 모든 정점에 대해 동일합니다. vc0-3은 모델 뷰 – 프로젝션 행렬을 포함하고 vc4는 현재 알파 값을 포함합니다.

모든 버텍스 쉐이더의 주요 임무는 꼭지점 위치를 소위 “clip-space, 클립 공간”으로 이동시키는 것입니다. 꼭짓점 위치에 mvpMatrix (modelview-projection 행렬)를 곱하면 됩니다. 첫 번째 라인은 이를 처리하고 Starling의 모든 버텍스 쉐이더에서 찾을 수 있습니다. 꼭지점이 화면에서 끝나는 곳을 알아내는 일은 책임이라는 말로 충분합니다.

그렇지 않으면 우리는 “다양한 레지스터” v0 – v2를 통해 프래그먼트 셰이더에 데이터를 전달하는 것보다 더 많거나 적습니다.

프래그먼트 셰이더는 필터 클래스에 상응하는 거의 동일한 복제본입니다. 차이점을 찾을 수 있습니까? 오프셋을 읽는 레지스터입니다. 이전에는 v2에서 상수에 저장되었습니다.

3.6.4. 시도해 보기

당신은 그것을 가지고 있습니다: 우리는 거의 우리 스타일로 끝냈습니다! 테스트 합시다. 진정으로 대담한 움직임에서, 저는 두 개의 객체에서 즉시 사용하므로 일괄 처리가 올바르게 작동하는지 확인할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
var image:Image = new Image(texture);
var style:ColorOffsetStyle = new ColorOffsetStyle();
style.redOffset = 0.5;
image.style = style;
addChild(image);

var image2:Image = new Image(texture);
image2.x = image.width;
var style2:ColorOffsetStyle = new ColorOffsetStyle();
style2.blueOffset = 0.5;
image2.style = style2;
addChild(image2);

그림 54. 두 개의 스타일이 지정된 이미지. 하나의 드로우 콜로 렌더링됩니다.

만세, 이건 실제로 작동합니다! 왼쪽 상단의 그리기 횟수를 확인하십시오. 이는 정직하고 지속적인 “1”입니다.

그래도 조금 더 할 일이 있습니다. 위의 셰이더는 항상 데이터를 읽을 텍스처가 있다고 가정하여 만들어졌습니다. 그러나 텍스쳐를 사용하지 않는 메쉬에 스타일을 할당할 수도 있습니다. 그래서 우리는 이 케이스를 위한 특정 코드를 작성해야 합니다 (너무 간단해서 지금 당장 그것에 대해 자세히 설명하지 않을 것입니다).

이 마지막 순간의 수정을 포함한 전체 클래스는 여기에서 찾을 수 있습니다 : ColorOffsetStyle.as.

3.6.5. 이제 어디로 가야 하나

그것은 우리 스타일입니다! 나는 우리가 우리의 임무에 성공했다는 사실에 대해 당신이 기분이 흥분되기를 바랍니다. 위에서 보는 것은 상상력에 의해서만 제한되는 방식으로 Starling을 확장하는 열쇠입니다. MeshStyle 클래스는 슬리브를 조금 더 트릭하기 때문에 전체 클래스 문서를 읽으십시오.

저는 여러분이 무엇을 생각해낼지 기대하고 있습니다!