すこしふしぎ.

VR/HI系院生による技術ブログ.まったりいきましょ.(友人ズとブログリレー中.さぼったら焼肉おごらなきゃいけない)

【openFrameworks】oF for iOSでretina対応する(ためにoF初期化コードを読んでみた)話【ofxiOSAppWindow】

こんばんは.1000chです.

oFを利用するメリットの一つに,モバイル端末向けのアプリを簡単に開発できる,という点があります. oF for iOSだとか,oF for Androidとかですね. ユーザ操作イベントに関するロジックとかは別個になってしまいますが,メインロジックに関しては共通コードで開発することができます. Mac上で作ったアプリがiPhone上で動くと,ちょっとした感動がありますね.

で僕もよくoF for iOSiPadアプリ作ってたりするんですが,デフォルトでは画面解像度が1024*768で固定だったりします. これだと,せっかくiPad Airとか使っても画像がガクガクなんですよね. 今回これをretina仕様に変更する方法を知ったのでメモっておきます.

【記事書き終わって追記】 コード読みながらgdgd書いてたらとんでもない長さに... シンプルに結論だけ気になる方はこちら

普通に解像度変更しようと挑戦する

画面サイズの指定等はmain.mm中のofSetupOpenGLで呼ばれているはずなので,main.mmを見ていきます.

#include "ofMain.h"
#include "ofApp.h"

int main(){
    ofSetupOpenGL(1024,768, OF_FULLSCREEN);
    ofRunApp(new ofApp);
}

ofSetupOpenGLで画面サイズを指定してる感じですね.試しにsetup内に以下を記述してみましょう.

void ofApp::setup(){
    cout << ofGetWidth() << ":" << ofGetHeight() << endl;
}
// 出力
768:1024

うむ,それっぽいです.

ちなみにofSetOrientation(OF_ORIENTATION_90_LEFT)叩いた後だと,この値も入れ替わります.

void ofApp::setup(){
    ofSetOrientation(OF_ORIENTATION_90_LEFT):
    cout << ofGetWidth() << ":" << ofGetHeight() << endl;
}
// 出力
1024:768

じゃーここに与える数字変えれば終わりじゃん!らくしょう!

やってみます.retina解像度である2048*1536を入れてみましょう.

#include "ofMain.h"
#include "ofApp.h"

int main(){
    ofSetupOpenGL(2048,1536, OF_FULLSCREEN);
    ofRunApp(new ofApp);
}

さぁどうだ.

void ofApp::setup(){
    ofSetOrientation(OF_ORIENTATION_90_LEFT):
    cout << ofGetWidth() << ":" << ofGetHeight() << endl;
}
// 出力
1024:768

なん...だと...???

コレ実は,Mac上のoFではフツーに動作して画面サイズ変えられます. おそらくiOSMacで,初期化処理が異なっているのでしょうね.

retina対応しているサンプル見つけた

てなわけでしばらくretina対応はあきらめていたのですが,いろいろサンプルコードを動かしている中で「あれ..これ明らかに解像度高くね?」というものを見つけてしまったのです. それがexample/ios/fontsExample

こんなもんを見つけてしまった以上,コードがどうなってるのかを見ない訳にはいきませんね.

早速main.mmを見てみましょう.

#include "ofMain.h"
#include "ofApp.h"

int main(){
    ofAppiOSWindow * iOSWindow = new ofAppiOSWindow();
    
    iOSWindow->enableAntiAliasing(4);
    iOSWindow->enableRetina();
    
    ofSetupOpenGL(iOSWindow, 480, 320, OF_FULLSCREEN);
    ofRunApp(new ofApp);
}

ぼくの知ってるoF for iOSの初期化と全く違う!なんぞこれ!

ofAppiOSWindowを読む

どーやらofAppiOSWindowクラスが鍵を握っていそうですね.

ということでコードを読んでみると,ofAppiOSWindowofAppBaseWindowのサブクラスのよう.virtual使いまくってるところを見ると,ofAppBaseWindowが抽象クラスとして存在して,Mac,Win,iOS,android etc.向けに,それぞれサブクラスが定義されているのでは,という感じがします.

実際ofSetupOpenGLの定義をのぞいてみると

// ProjectGeneratorで作ったときによばれてるやつ
void ofSetupOpenGL(int w, int h, int screenMode){
        // 利用デバイスにあわせてwindow インスタンスへのポインタを生成
   #ifdef TARGET_NODISPLAY
        window = ofPtr<ofAppBaseWindow>(new ofAppNoWindow());
   #elif defined(TARGET_OF_IOS)
        window = ofPtr<ofAppBaseWindow>(new ofAppiOSWindow());
   #elif defined(TARGET_ANDROID)
        window = ofPtr<ofAppBaseWindow>(new ofAppAndroidWindow());
   #elif defined(TARGET_RASPBERRY_PI)
        window = ofPtr<ofAppBaseWindow>(new ofAppEGLWindow());
    #else
        window = ofPtr<ofAppBaseWindow>(new ofAppGLFWWindow());
   #endif

    ofSetupOpenGL(window,w,h,screenMode); //windowへのポインタを下の関数に渡す
}

// 普段は上の関数でwindowポインタが生成され,コレがよばれる
// 先のmain.mmではwindowのポインタを直接指定してコレをよんでた
void ofSetupOpenGL(ofPtr<ofAppBaseWindow> windowPtr, int w, int h, int screenMode){
    // 現在レンダラー情報の取得?
    if(!ofGetCurrentRenderer()) {
   #ifdef USE_PROGRAMMABLE_GL
        ofPtr<ofBaseRenderer> renderer(new ofGLProgrammableRenderer(false));
   #else
        ofPtr<ofBaseRenderer> renderer(new ofGLRenderer(false));
   #endif
        ofSetCurrentRenderer(renderer,false);
    }

    window = windowPtr; // windowはofAppRunnerのstatic変数

    if(ofIsGLProgrammableRenderer()){
        #if defined(TARGET_RASPBERRY_PI)
        static_cast<ofAppEGLWindow*>(window.get())->setGLESVersion(2);
       #elif defined(TARGET_LINUX_ARM)
        static_cast<ofAppGLFWWindow*>(window.get())->setOpenGLVersion(2,0);
       #elif !defined(TARGET_OPENGLES)
        static_cast<ofAppGLFWWindow*>(window.get())->setOpenGLVersion(3,2);
       #endif
    }else{
       #if defined(TARGET_LINUX_ARM) && !defined(TARGET_RASPBERRY_PI)
        static_cast<ofAppGLFWWindow*>(window.get())->setOpenGLVersion(1,0);
       #endif
    }

    window->setupOpenGL(w, h, screenMode); // 今回の場合oFAppiOSWindowのsetupOpenGLを呼ぶ
}

と呼ばれていくようで,結局のところ各サブクラスでオーバーライドしたsetupOpenGLを 呼ぶ,というところがポイントのようです.ということでのぞいてみるも,

void ofAppiOSWindow::setupOpenGL(int w, int h, int screenMode) {
    windowMode = screenMode; // use this as flag for displaying status bar or not
}

まさかのwhも利用していないというね. そりゃー値指定してもサイズ変わらん訳ですね. ココでは関数名の通り,OpenGLの設定が主な処理の様です.

話がすこしそれましたので,main.mmに戻りましょう.コードを再掲します.

#include "ofMain.h"
#include "ofApp.h"

int main(){
    ofAppiOSWindow * iOSWindow = new ofAppiOSWindow();
    
    iOSWindow->enableAntiAliasing(4);
    iOSWindow->enableRetina();
    
    ofSetupOpenGL(iOSWindow, 480, 320, OF_FULLSCREEN);
    ofRunApp(new ofApp);
}

あきらかに怪しいのがiOSWindow->enableRetinaですね. 改めてofAppiOSWindowのヘッダをのぞくと,独自メソッドとして以下を持つようです.

    //-------------------------------------------- ios config.
    bool enableHardwareOrientation();
    bool disableHardwareOrientation();

    bool enableOrientationAnimation();
    bool disableOrientationAnimation();
    
    bool enableRendererES2();
    bool enableRendererES1();
    bool isRendererES2();
    bool isRendererES1();
    
    // こいつらが今回のカギ!
    bool enableRetina();
    bool disableRetina();
    bool isRetinaEnabled();
    bool isRetinaSupportedOnDevice();
    
    bool enableDepthBuffer();
    bool disableDepthBuffer();
    bool isDepthBufferEnabled();
    
    bool enableAntiAliasing(int samples);
    bool disableAntiAliasing();
    bool isAntiAliasingEnabled();
    int getAntiAliasingSampleCount();

方向変化でのアニメーション,アンチエイリアスの設定などもあるみたいですね. enableRetinaメソッドをみてみましょう.

bool ofAppiOSWindow::enableRetina() {
    if(isRetinaSupportedOnDevice()) {
        bRetinaEnabled = true;
    }
    return bRetinaEnabled;
}

retina有効フラグをtrueにするメソッドのようで,コレを呼ぶことによりoF for iOSでも アプリをretina対応できる,ということみたい.

ただ,サンプルの様に毎度呼んでしまうのはアレなので,isRetinaSupportedOnDevice()を利用してretian対応デバイスかのチェックはしたほうが良いかもしれないです. こんな感じかな.

#include "ofMain.h"
#include "ofApp.h"

int main(){

    ofAppiOSWindow * iOSWindow = new ofAppiOSWindow();
    
    if (iOSWindow->isRetinaSupportedOnDevice()) {
        iOSWindow->enableRetina();
    }
    
    ofSetupOpenGL(iOSWindow, 1024, 768, OF_FULLSCREEN);
    ofRunApp(new ofApp);
}

結論

oF for iOSretina対応したければ,

  1. ofAppiOSWindowインスタンス生成して
  2. enableRetinaメソッド呼んで
  3. ofSetupOpenGLにこのインスタンスを渡す

でいけそう!

#include "ofMain.h"
#include "ofApp.h"

int main(){
   // 1.instance生成
    ofAppiOSWindow * iOSWindow = new ofAppiOSWindow();

   // 2. enableRetina()    
    iOSWindow->enableRetina();

    // 3. ofSetupOpenGL
    ofSetupOpenGL(iOSWindow, 480, 320, OF_FULLSCREEN);
    ofRunApp(new ofApp);
}

余談:ところでretina対応のルーティングどこでやってんの?

「どこでこのフラグ使ってルーティングしてるの?」が気になったのですが, 先程見たようにofSetupOpenGLではretina対応のルーティングはありませんでした. そこでbRetinaEnabledgrepするも,それっぽい条件分岐がみつからないという始末...

あっれー?

てことで,いわゆるiOSでのretina対応はどうするのか?をぐぐってみました.

アプリケーションを iPhone 4 の Retina Display に対応するための方法いろいろ - 24/7 twenty-four seven

OpenGL ESのRetina対応 | Objective-Audio

oFは基本OpenGL ESを利用しているので.2つめの記事が参考になるかも.曰く,

iOS4からUIView(およびEAGLViewなどのサブクラス)にcontentScaleFactorというメソッドが追加され、この値を変更することでUIViewの中の解像度を変更することができるようになりました

(中略)

glView.contentScaleFactor = [UIScreen mainScreen].scale;

基本、これだけでRetinaディスプレイのiPhone4だと倍の解像度になる

ほう. ならばcontentScaleFactorで探してみましょう.

発見!

// EAGLViewクラス
// 引数にretinaのフラグが!
- (id) initWithFrame:(CGRect)frame
 andPreferedRenderer:(ESRendererVersion)version
            andDepth:(bool)depth
               andAA:(bool)fsaaEnabled
       andNumSamples:(int)samples
           andRetina:(bool)retinaEnabled{

    if((self = [super initWithFrame:frame])) {
        
        rendererVersion = version;
        bUseDepth = depth;
        bUseFSAA = fsaaEnabled;
        bUseRetina = retinaEnabled; // retinaのフラグ!
        fsaaSamples = samples;

        //中略

        
         // retinaのルーティング!
        if(bUseRetina){
            if([[UIScreen mainScreen] respondsToSelector:@selector(scale)]){
                if ([[UIScreen mainScreen] scale] > 1){
                    [self setContentScaleFactor:[[UIScreen mainScreen] scale]];
                    scaleFactor = [[UIScreen mainScreen] scale];
                } else {
                    bUseRetina = false;
                }
            } else {
                bUseRetina = false;
            }
        }

さらにこんなのも.

// ofxiOSEAGLViewクラス.EAGLViewのサブクラス
- (id)initWithFrame:(CGRect)frame andApp:(ofxiOSApp *)appPtr {
    
    ESRendererVersion preferedRendererVersion = ESRendererVersion_11;
    if(ofIsGLProgrammableRenderer()) {
        preferedRendererVersion = ESRendererVersion_20;
    }
    
    self = [self initWithFrame:frame
           andPreferedRenderer:preferedRendererVersion
                      andDepth:ofAppiOSWindow::getInstance()->isDepthBufferEnabled()
                         andAA:ofAppiOSWindow::getInstance()->isAntiAliasingEnabled()
                 andNumSamples:ofAppiOSWindow::getInstance()->getAntiAliasingSampleCount()
                     andRetina:ofAppiOSWindow::getInstance()->isRetinaEnabled()];

     // 略

bRetinaEnabledgrepしても見つからないわけですね.クラスメソッドgetInstanceインスタンス呼んで,getterのisRetinaEnabled呼んでいたとは..

そしてofxiOSEAGLView自体はofxiOSViewControllerで生成されていました.

// ofxiOSViewControler
- (id)initWithFrame:(CGRect)frame app:(ofxiOSApp *)app {
    currentInterfaceOrientation = pendingInterfaceOrientation = UIInterfaceOrientationPortrait;
    if((self = [super init])) {
        currentInterfaceOrientation = pendingInterfaceOrientation = self.interfaceOrientation;
        bReadyToRotate  = NO;
        bFirstUpdate    = NO;
        
       // ofxiOSEAGLViewの生成!
        self.glView = [[[ofxiOSEAGLView alloc] initWithFrame:frame andApp:app] autorelease];
        self.glView.delegate = self;
    }
    
    return self;
}

objective-cでのiOSアプリ開発あまりやったこと無いので僕のコードリーディングはここで力つきました..orz

ofxiOSAppDelegeteでこのViewControllerが指定されてたり,ofRunAppの先でUIApplicationMainofxiOSAppDelegateを呼べ!的な処理があるので,おそらくは上手いことこのviewが生成されるところまでつながっているんでしょう←

obj-cのios開発を勉強してからまた出直そうと思いますw