성능에 따른 렌더링 주기 조정

최근 들어 매우 난감하고 어려운 상황이 한 가지 있습니다. 사태의 심각성을 이해하기 위해 먼저 제 블로그 방문자의 화면 해상도 통계를 보죠.

1

이 통계는 제 블로그의 방문자들이 고해상도 모니터를 갖춘 강력한 pc로 방문하신다는 의미입니다. 저도 물론 6번에 해당되는 30인치 고해상도 모니터와 강력한 데스크탑을 갖고 있습니다. 제가 모기업 포탈의 메인 담당자였을 때가 약 7년 전인데 그 때 분위기만해도 방문자가 점점 고사양, 고해상도로 변화되면서 노트북도 1280이상으로 가는 추세였습니다. 따라서 2년만 지나면 1280기반으로 웹이 이전할 줄 알았습니다. 그리고 그 이후 많은 시간이 지나면서 알게 모르게 ‘웹은 1280이 주류일거야 ‘라는 생각을 하고 있었죠.

하. 지. 만. 실제 서비스의 현실은 아래와 같습니다.

0

많은 웹서비스의 통계를 보면 1024의 시대가 다시 열리고 말았습니다. 그것은 결국 아톰이 사이트에 들어온다는 의미입니다.  따라서 화면 해상도 뿐만 아니라 저사양 cpu에 플래시를 최적화해야한다는 의미이기도 합니다.

3D와 아톰

그러니까 아톰이라도 코어듀오에 비해 정말 많이 불리한 건 아니고 그저 CPU가 느린 만큼만 불리합니다. 짜피 플래시가 GPU를 안쓰기 때문에 오히려 성능 차이를 고려 할 만 합니다. 특히 3D를 구현하기 위해 사용하는 PV3D, AWAY3D 등은 무지막지한 CPU 연산을 할 뿐이라 오히려(?) 다행입니다. 실제로 GPU차이를 일으키는 많은 현대 게임들은 아예 실행자체가 불가능하니 그에 비하면 다행이란 거죠.

3D에서 가장 많이 CPU를 잡아먹는 부분은 어디일까요? 그것은 당연하게도 render함수를 호출하는 것입니다. 따라서 얼마나 render함수를 덜 호출하느냐가 관건입니다. 문제는 이걸 덜 호출할수록 화면은 애니메이션이 아니라 슬라이드쇼로 변해간다는 점입니다.

가장 이상적인 시나리오는 성능이 우수한 컴터은 자주 호출하고 낮은 컴터는 뜸하게 호출하는 것입니다. 이것을 초기설정이 아닌 실시간 반영되게 개념을 확장하면

현재 프레임레이트를 고려하여 render를 호출한다.

로 정리할 수 있습니다. 이 방법은 전통적으로 게임렌더링 엔진이 채용하는 방식입니다. 간단히 render함수 내부를 보겠습니다.

private function ENTER_FRAME( $e:Event ):void{
	var time:int;
	if( _isPlay ){
		time = getTimer();
		if( time > _ms ){
			_ms = time + 1000;
			_rate = 1000 / _fps;
			_fps = 0;
		}
		++_fps;
		if( time > _curr ){
			if( _view.stage ){
				_view.render();
			}
			time = getTimer();
			_curr = ( time + _rate ) + ( time - curr );
		}
	}
}

로직은 간단하지만 복잡한 변수관계를 갖고 있기 때문에 주의깊게 보셔야 합니다.

먼저 isPlay플래그를 이용해 활성, 비활성상태에 적극적으로 대처합니다. 매우 중요합니다. 이러한 분기 플래그가 있으면 아무래도 자주 렌더링은 온오프하게 됩니다. 애니메이션이 끝나면 빨리빨리 오프로 만들어주는게 좋습니다.

다음 if블럭은 fps를 측정하고 거기에 맞춰 재생 빈도를 조정하는 역할을 합니다.

  • fps란 1초당 몇 번이나 enterframe이 실행되었나를 측정합니다.
  • 따라서 처음부터 현재 시간에 1초를 더해 _ms에 저장해두면 그저 getTimer()와 비교할 수 있습니다.
  • fps를 측정한 후에는 다음 측정을 위해 fps를 0으로 초기화합니다.
  • _rate를 fps기반으로 갱신합니다.

이 블럭에서 가장 중요한 것은 측정한 fps를 기반으로 _rate를 조정한다는 점입니다. _rate는 얼마나 자주 render를 호출할지 결정하는 중요한 변수입니다. 샘플코드에서 _rate의 단위는 ms입니다.

그 다음 if는 _rate에 맞춰 render를 호출합니다. 또한 render호출 후에 지연을 계산하여 다음 render호출을 더 미루는 역할도 합니다.

  • 예를들어 _rate가 30ms라 하면, render는 30ms마다 호출되어야합니다.
  • 하지만 실제 render호출 후 getTimer를 측정해서 ’40ms만에 실행되었다’ 라는 결과가 나왔다면
  • 짜피 이 컴은 30ms마다 실행할 능력이 없다는 뜻입니다. 따라서 차이인 10ms만큼을 더해 다음번 render호출은 30ms가 아닌 40ms 후에 하게 됩니다.
  • 원래 다음 실행주기는 time+rate지만 현재 지연시간은 time – curr 이므로 이를 반영하게되면 ( time + _rate ) + ( time – curr ) 가 얻어집니다.

이러한 로직은 실시간으로 성능을 측정하여 성능만큼 render를 덜 호출하게 하는 효과를 갖고 옵니다. 왜 이런 로직을 사용하는가가 중요합니다.

  • 만약 이러한 방식을 채용하지 않고 enterframe에 그냥 render를 걸어둔 경우 실행은 점점 지연되게됩니다.
  • enterframe에 걸린 리스너가 실행을 지연시키는 경우 전체적인 플래시의 실행능력은 같이 낮아집니다.
  • 이 결과 1초동안 500픽셀을 이동해야하는 애니메이션의 경우 컴터 CPU에 따라 다음과 같은 형태로 처리됩니다.
  • 좋은 컴터: 1초동안 500픽셀을 이동하고 부드러운 애니메이션이 일어난다.
  • 나쁜 컴터: 5초동안 500픽셀을 이동하고 끊어지는 애니메이션이 일어난다.

위와 같은 시나리오는 게임시스템같은 실시간 어플리케이션에서는 최악입니다. 하지만 render에 fps를 반영하면 아래와 같이 변합니다.

  • render실행능력만큼 render가 지연된다.
  • 따라서 enterframe지연이 최소화된다.
  • 이 결과 시간 기반으로 계산되는 트위닝이나 수치적인 변화를 render시마다 반영한다.
  • 좋은 컴터: 1초동안 500픽셀을 이동하고 부드러운 애니메이션이 일어난다.
  • 나쁜 컴터: 1초동안 500픽셀을 이동하고 끊어지는 애니메이션이 일어난다.

결국 fps를 반영하는 의미는 같은 시간 내에 목표에 도달하고 대신 나쁜 컴터가 render를 상대적으로 덜 호출하게 만들어 좋은 컴터 효과가 부드러운 애니메이션에 반영되도록 해주는 효과입니다. 따라서 완벽하지는 않지만 성능차가 상당부분 극복됩니다. 또한 나쁜 컴터가 완전히 버벅거리는 것도 많이 개선됩니다.

결론

더욱 중요한 사실은 이러한 컴터의 성능차이에 대해 개발자가 신경쓰지 않고 런타임에 직접 평가하여 반영시킨다는 점입니다. fps를 계산하거나 _rate를 처리하거나 getTimer를 두 번 호출하는 부하 따위는 render가 한번 덜 호출되는 효과에 비하면 비용도 아닙니다.
하지만 여전히 플래시는 약점이 많습니다. 좀 더 시스템적인 언어의 경우 렌더링처럼 병목구간을 만드는 쓰레드를 분리하여 지연실행하고, 인공지능처럼 일정하게 처리되나 신속하게 계산되는 로직은 별도의 쓰레드에서 안정적으로 돌리는 식으로 개발할 수 있습니다. 이빨이 없으면 잇몸이라고.. 어쨌든 대략 저사양에서도 굴러가는 3D를 만들어가고 있습니다.

P.S 덕분에 넷북을 한 대 사버리고 말았습니다.

Browser does not supports flash movie


관련된 글:

  1. Bitmapdata로 Blitting를 구현하기
  2. TAF로 개발된 샘플 #2
  3. 키코드값표
  4. swf로 ajax를 대신하기
  5. Timebased Animation Interface

24 Comments

  1. 라피르 says:

    아 글올라온건 주말부터 알았지만, 출근해서 읽을려고 아껴뒀더랬죠 ㅋ..

    유익한 글 감사드리옵니다~!

  2. 꼬깨미 says:

    해상도가 뒤로 가고 있다…
    의미 심장한 내용이네요 ^^
    좋은글 잘봤습니다.
    그런데 넷북을 지르셨다능 ㅋㅋㅋ
    저도 넷북을 사용해보긴 했으나
    해상도 압박으로 바로 방출했었는데…
    일반 유져들은 무리 없이 사용하나봅니다…
    역시나 유져들에 패러다임이 바뀌면…
    개발방법도 바꿔야겠죠…

  3. 웹눈 says:

    찾아보니까 아이패드도 1024*768 이네요.
    좋은글 감사합니다.

    • admin says:

      실제 넷북의 대부분은 1024*600인데 구글통계가 그냥 768로 찍는건지 아니면 넷북 브라우저가 그렇게 보고하는건지 오늘 함 확인해봐야겠습니다.

  4. 지돌스타 says:

    저의 경우에도 비슷합니다. 대신 1680×1050사용자와 1280×1024 사용자가 거의 같네요.
    역시 서비스를 하는 입장에서 다양한 사용자 pc를 최대한 고려할 필요가 있겠어요.

  5. 박동준 says:

    저는 웹 사이트를 만들때 1280*1024에서 하고 최소한으로 1026*768에 까지 잘보이게 작업을 합니다..

    그런데 좀 주제랑 다른 애기지만..^^
    가끔 일을 하다보면 저나,고객은 화면에 많은 기능을 담으려고 합니다.
    그래서 괴롭죠^^
    화면이 작아지면 더 괴롭겠네요..^^

    • admin says:

      화면의 사이즈가 컨텐츠를 제약하면 그 뒤에 더 많은 일이 따라옵니다.
      조작성이 불편해지거나 컨텐츠 자체가 허접해지거나 무거운 어플리케이션이 되기 쉽죠.

      게다가 보통 작은 사이즈의 화면은 CPU가 나쁜 컴인 경우도 많기 때문에 이 악순환은 더욱 고조됩니다 ^^

      뭐 그정도죠

  6. 지돌스타 says:

    렌더링과 별도로 좌표값과 같은 시간종속적인 속성들은 Framerate 수준으로 변해야겠네요.

    • admin says:

      그래서 전체적으로 마스터 루프 함수가 시간베이스로 호출되고 모든 로직은 시간베이스로 역산하는게 최고입니다.
      예를들어 트위닝함수들은 대표적으로 보간방식을 사용하잖아요.
      사실 모든 알고리즘이 보간방식으로 작동해야 컴터간의 성능차이에 영향을 가장 덜받게 되는거죠.

      • jin_u says:

        일반 코드는 코드를 줄이기 위해서 뭉쳐서 코드하지 말고, 풀어해친 단순한 인라인 방식.
        연산 코드는 비트(바이트) 연산으로 처리.
        모션 코드는 EnterFrame으로 타임 베이스로 역산하는 트윈 방식.

        머 이정도만 되어도 화면을 빨리 그려주는 최고의 방식이라고 생각되는군요.

        느려지게 만드는 메서드나 프로퍼티들은 왠만하면 사용하지 않는 방법도 많이 있겠고,
        옛날이 생각나는 군요. Flash6까지는 ASnative라는 직접적으로 명령 셀에 접근하는 코드가 있었더랬죠. ^^

        좋은 내용들이 있어 나도 모르게 제가 알고 있는 짧은 지식들을 조금씩 남기고 가네요.
        자제 자제… -_-;

        • admin says:

          자제하실 필요없다는. 지식은 공유할수록 즐거우니까요.

          단지 이 포스트는 최적화에 대한 게 아니라 말그대로 렌더링 주기 조정인데, 일반적인 경우라기보다는 PV3D등 render를 호출하는 상황을 상정하고 쓴 부분도 다분히 있습니다.

          최적화는 음, 오래되어서 가물거리는데 아마 최적화로 검색하면 블로그에 두어개정도의 포스트가 ^^

  7. 지돌스타 says:

    이 글에 더 추가해야 되는 건 frameRate를 최대로 올려주어야 한다는 것 아닐까요?

    만약 frameRate를 24로 지정했을 경우 그것 자체의 해상도가 40ms(=1000s/24fps)정도가 됩니다. 어떤 로직을 50ms마다 실행해야한다고 가정한다면 40ms 해상도 안에서는 50ms마다 동작하는 로직은 해상도가 너무 큰거죠. 그래서 최대 50ms~90ms마다 로직이 실행되는 문제가 발생합니다.

    frameRate를 100으로 상향조절하면 해상도가 10ms정도가 되고. 같은 50ms 마다 실행되어야하는 로직은 50ms~60ms 정도의 로직 실행빈도를 나타내어 거의 정상적으로 실행되게 되겠죠.

    제가 이해하고 있는 것이 맞나요?

  8. 지돌스타 says:

    아~ 제 생각이 맞군요. ^^;

    그럼 이런 문제가 있을 것 같습니다.
    일단 히카님 회사에서는 완성된 게임 및 애플리케이션을 납품하는 입장에서는 이 방법이 가장 명확한 렌더링 주기 설정 방법인 듯합니다. 하지만 이 경우 만약 라이브러리를 제공해야하는 입장에서 이 방법을 사용하는 것은 무의미 할 것 같아요. frameRate를 임의로 조정해 버리는 것 자체에 부담을 줄 수 있기 때문이지요.

    이 글의 주제와 범위에서 벗어나긴 하지만 이것에 대해서 좀 연구하다가 알아낸건데요…. 예전에 bs-시리즈 라이브러리는 하나의 완성된 애플리케이션을 완성하는데는 맞는것 같습니다. 하지만 제가 이것을 이용해서 다른 라이브러리를 만드려고 하니 문제가 있더라구요. 왜냐하면 bs-core가 CS를 중심으로해서 static 구조로 만들어져 있기 때문인데요. 가령 지도를 담은 라이브러리 만든다고 할때 그 라이브러리 사용자는 그 지도의 인스턴스를 2개이상 만들어 사용해야하는 니즈를 만족시키기는 힘들더라구요. 오픈 API등을 제공하기 위해서는 이 방식으로는 힘든 것 같습니다. ^^

    • admin says:

      하나씩 설명을 드리면, 먼저 프레임레이트의 문제인데,
      1. 프레임레이트는 결국 그 결과로 무언가를 호출하는 게 결론입니다. 그 무언가에 아무것도 넣지 않는 이상 부담을 줄 일은 없겠죠.
      2. 엔터프레임은 사실 stage의 enter에서 모든걸 처리하는 제 프레임웍의 특성에서 나온거지 Timer를 쓰면 안된다는 의미는 아닙니다. 당연히 배포용이라면 Timer를 기반으로 하는 편이 안전하겠죠.

      CS는 Static으로 작동하지 않습니다. CS는 근본적으로 Main이 Sprite대신 상속할 목적으로 만들었기 때문에 다른 클래스들과 달리 추상부모로서 작동하는게 기본입니다. 또한 Main전용으로 만들기는 했지만 현재 Main.as가 최초에 로딩될 swf의 마스터가 아니라면 슬레이브모드를 설정하여 stage관련 설정을 하지 않도록 되어있으므로 사실 상 아무 Sprite 대신에 부모 클래스로 사용할 수 있는 셈입니다.

      CS가 static이라고 느낀 부분은 어떤 부분일까요? embed자원쪽인가..activeMode나 키이벤트 처리도 slave로 분리해버리면 다 무시하도록 되어있을텐데…게다가 core패키지의 다른 클래스가 CS에 의존적이지 않을텐데 ^^; ㅎㅎ 갖고 계신 버전이 어떤건지 잘 몰라서 ^^;

      여하튼 이걸 다 떠나서 지도를 담은 라이브러리는 Main클래스가 아니잖아요. 따라서 CS를 상속할리도 없고. 따라서 Main이 아니니 CS가 정적이든 아니든 영향을 받을 이유는 없을텐데 이 부분이 잘 이해가 안갑니다.

  9. 지돌스타 says:

    네~ 저도 그래서 Timer를 생각했습니다. 단지, 제약점이 있는 것 같아 의견을 듣고 싶어서 말했던 겁니다. ^^

    구체적으로 말씀드려야겠네요.

    제가 문제로 삼은것은 Runner부분입니다. 이게 static으로만 동작하도록 되어 있거든요. 저는 지도에 CS를 상속하려고 하는 상황에서 Runner부분을 활용하려다가 문제점이 있는 것 같아서요.

    만약 Map을 사용할때 addRunner로 함수를 등록했다가 Map이 이제 더이상 필요없게 된다면 removeToStage 될시에 removeRunner를 호출해서 다 빼주어야하는 건가요? static 으로 만들지 않고 Resouce 처럼 활용할 수 있는 방안도 있지 않았을까 의문을 가진겁니다.

    그래서 결국 문제가 될 것 같아 bs-core 부분만 제외하고 필요한 부분을 Map라이브러리에 따로 클래스를 만들어 정리했습니다.

    • admin says:

      오 그렇게 생각하니 러너가 좀 스태틱으로 도는데다가 stage의 이벤트에 많이 의존적인거 같아요. 사실 Cresource도 그런 문제로 CS에서 C로 분리한뒤 소유모델로 전환한거죠. 전 퍼포먼스로 러너를 신중하게 등록하다보니 한 번도 러너를 인스턴스별로 가져간다는 생각을 안해봤네요 ^^;
      하지만 CS를 전체적으로 응용하는 건 둘째치고 러너랑 비슷한 층을 Map라이브러리가 static으로 갖고 있는 건 자원관리 상 좋을 듯도 해요 ^^ 짜피 맵을 원하는 사람들은 맵을 임포트할테니 맵이 로딩된 시점에서 모든 맵 인스턴스들이 공유할 함수나 컨테이너는 맵패키지 안에 정적으로 있으면 좋을듯.

      그리고 걍 위의 질문의 답인데 맞아용 addRunner는 엄격한 removeRunner를 요구해요. 최초 설계를 stage에 거는 이벤트라고 생각해서 한정적이고 엄격하게 관리할 생각이었거든요.

      그리고 TAF에선 방금 말씀하신 러너의 불합리함 + 원더플에서 하셨던 파티클을 위한 벡터루프의 시사점을 응용하여 새롭게 작성되어서 그건 또 TAF의 렌더링 처리라는 글로 정리해서 올려보겠습니다.

      그리고 타이머는 성능 상의 문제는 타이머를 여러개 쓰지 않는 이상 거의 없습니다. 오히려 보간을 많이 쓰는 경우 TimerEvent가 리스너에서 귀중한 updateAfterEvent 를 제공해서 유리한 점도 많이 있습니다 ^^

  10. 지돌스타 says:

    아~ 그렇군요. 답변 감사~ ^^

    Runner를 등록할때 신중하게 하신다는 의미는 이해하겠습니당. 특히나 render기능을 가진 Runner함수를 등록하면 enterframe마다 호출되므로 쓸데없는 자원낭비를 요구할 수 있으므로 신중히 해야겠죠. Runner를 필요할때마다 등록하고 삭제하는 방식이 가장 좋겠지만 그렇게 되면 호스트코드의 복잡도가 증가할 수 있으므로 초반에 필요한 Runner를 다 등록해놓고 Runner자체에 자신의 동작시기를 알 수 있도록 하는 것이 좋지 않을까요? 가령….
    function runner1():void {
    if(isPlay===false) return;
    ….실제 동작….
    }

    isPlay가 true이어야만 이 Runner함수가 동작하므로 이런 Runner가 10~20개가 된다한들 함수호출정도이므로 퍼포먼스상에는 문제가 없을 것 같은데… 물론 이 10~20개 함수가 한꺼번에 실제 동작되는 일은 없고 2~3개 정도가 선택적으로 동작하도록 되는거죠. 어떻게 생각하시는지…

    그리고 말씀하신데로 저의 경우 지도별 유일한 타이머를 사용해 render를 동작하도록 하고 updateAfterEvent를 이용하는게 좋겠죠? 24fps정도는 나와야하므로 40ms(=1s/24fps) 주기로 타이머를 돌려도 문제가 없다는 말이죠? updateAfterEvent를 잘못사용하면 오히려 퍼포먼스 문제를 야기시키지 않나 우려가 되서.. 생각을 못하고 있었는데, 저와같은 상황에서는 선택의 여지가 없어보이기도 하네요.

    • admin says:

      아마 갖고 계신 버전의 CS가 러너를 처음 생각해냈던 시점의 버전이었던거 같아요.
      그 러너엔 아마도 activate이벤트랑 render가 묶여있지도 않고 fps도 러너수준에서 관리 안하던 시절일 거에요.

      그 이후 제가 만드는 대부분의 어플은 swf가 포커스를 잃어버리면 render를 자동으로 정지하는 기능이 들어있는데 이게 매번 수동으로 구현하기 귀찮아서 렌더러너가 엑티베이트 디엑티베이트에 반응하게 수정되어있거든요.

      렌더러너가 생각보다 많이 등록되는 이유는 렌더함수가 여러개 객체에 분산되어있기 때문입니다. 아래와같은 상황입니다.

      CSbitmapBlit.render( ‘a’ );
      pv3dView1.render();
      pv3dView2.render();
      away3dView1.render();
      away3dView2.render();

      전 성능상의 이유로 3D쪽도 여러개의 뷰를 만들어 모델링을 분산한뒤 애니메이션이 필요한 부분을 z축기준으로 분해해보고 원하는 층만 애니메이션에서 render를 호출하고 나머지 층은 render를 더이상 호출하지 않고 고정된 컨텐츠로 남아있게 하는 방식을 많이 쓰기 때문에 생각보다 상당히 많은 함수가 렌더에 들어갔다 나갔다 합니다.

      두번째로 Timer의 사용하는 측면에서도 제 의견은 걍 rate는 1로 줘서 최대 속도를 확보한 상태에서 내부에서 if를 통해 render호출주기를 정하시는게 더 좋을지도 모르겠다는 뜻이었습니다. 1은 너무하지만 한 10정도로.

      사실 render이외의 계산은 루프상에서 거의 부하가 없는거나 마찬가지라 그리 신경쓸 대상은 아닙니다 ^^

  11. CINNABAR says:

    지돌스타님과 다이버스터님의 만담(?)은 보고만 있어도 공부가 되네요 ㅎㅎ

Leave a Reply