2013年10月29日 星期二

做 app 如何賺錢?先問自己五個問題

http://www.inside.com.tw/2013/05/13/how-to-make-money-by-making-apps

儘管過程艱難,但我們終究學到了如何聰明獲利。自從兩年前我們第一個版本的 Brainscape 誕生以來,我們陶醉於教師與學生對它的簇擁,卻幾乎忘記了獲利也同樣重要。
不過我們還是領悟必須在使用者成長與獲利之間取得平衡的道理。使用者當然希望每個 app 都是免費或者只要 99 美分,即便這些人願意每天去 2 次星巴克消費 5 美元的超大杯拿鐵。我們需要向使用者展現出我們 app 的價值。
讓我欣慰的是,通過學習 app 商店獲利最​優化的 Brainscape 已經解決了這個問題。過去 2 年裡,我們和許多行動創業公司 CEO 喝咖啡、啤酒,討論他們的獲利模式的戰略問題。不計其數的表格、文件見證了我們對獲利模式的試驗,我們甚至開展了這方面的教學,並向我們自己的學生學習。 我們的獲利最​優化的結果顯示,Brainscape 每個 app 下載的收入變成原來的4倍,而且我們可以持續地利用所得收入促進我們產品的發展。
選擇正確的 app 獲利模式,我們必須先問問自己下面五個關鍵的問題:

在有人為你的 app 付費之前,你知道他們從 app 中穫得了什麼嗎?

如果你的 app 有很實用的功能,比如手寫計算器或針對考試準備的 app ,那麼潛在的使用者可能已經完全了解 app 的功能,才會付費去下載該 app 。否則,你就只能讓你的 app 免費下載,然後再去找其它的獲利模式。之所以有人會花 5 美元去星巴克買大杯拿鐵,是因為他們清楚知道自己會得到什麼,而花 2.99 美元去買一個你還不了解的 app (即使該 app 的評價很高)?這跟賭博差不多。(所以如今最賺錢的 app 都是免費下載而使用內置收費(In-App Purchase)模式收費。)
你的 app 有千萬級的忠實使用者嗎?或者說你認為最終會達到這麼多的使用者嗎?(誠實地回答)
如果你的回答是肯定的,那麼你很可能透過廣告獲得收入。但記住,廣告不能幫助你取得特別高的回報,除非你的 app 有 Facebook 那種數量的深度使用者(每 1000 個行動廣告的瀏覽量只能為開發者帶來不到 1.5 美元的收入)。沒有龐大使用者基礎的 app 需要考慮有更高回報的獲利模式,如果他們想做大做強的話。

在使用你們的免費 app 時,使用者是否會突然感到極大的痛苦或渴望?

如果你有很堅實的使用者基礎,而且他們非常依賴你的 app 去獲得什麼東西,那麼你可以嘗試免費下載 / 內置收費的獲利模式。對於深度使用者,他們有些時候出於強烈的好奇心,會願意付費來達到更高的等級。這個更高的等級可以是提供更優質的內容、更優良的特 色、作弊手段、提示、武器裝備、更多條命、無限制的使用或者其它任何可以緩解使用者在某個時刻感到痛苦的東西。(最好是高價格或者打包購買)
比如,我之前沉迷於一個叫 Flow 的遊戲。每當我在遊戲中受挫,就會花 99 美分去購買提示。當我不再沉迷時,才發現我在這個遊戲的花費已經超過 20 美元了。
Brainscape 也提供了內置收費功能,用來為更優質的內容解鎖。我們允許使用者了解我們更高級的科目(比如外語、調酒和 GRE 應試),展示他們之前良好的學習效果,然後會詢問他們是否願意購買完整的學習資料。由於我們提供內容的高質量以及使用者學習的積極性,我們很多的科目的收 費都超過了 20 美元。
但是,必須注意的是,如果你的 app 內置了收費,一定不能有太多的免費項目,否則你的使用者不會「上鉤」。我們曾經做過個別應考科目免費使用的試驗,發現了 2 個問題:
1. 很多使用者只在考前 3、4​​ 天才下載這些應考 app ,所以我們提供的 5 天免費使用時間最終讓我們自己毫無報酬,只能苦笑了。
2. 把免費使用縮短到1、2 天會讓使用者很惱火,他們感到被欺騙,所以給了一星的差評。對 Brainscape 而言,使用內容解鎖的模式是更好的方式。
正確的內置收費模式取決於你們公司提供的內容。記住,內置收費模式對使用者行為非常敏感,所以通常也最適合在 app 內做試驗。

是否有某個不開發 app 的公司,它有價值很高的產品,而且覺得你的使用者對他們公司來說非常重要?

如果你的 app 有非常大的流量,這些流量不是給廣告商的,而是可以為一些公司提供潛在的客戶,那麼你就可能有很好的流量引導的獲利模式。基本的想法是你可以和這些需要客戶的公司洽談,你的 app 每為他們帶來一個合格的使用者,就可以收取一定的費用。
事實上,我也不太清楚有以這種模式取得巨大成功的公司。我覺得 Foursquare 可以算是一種,但他們也沒有很高的獲利。如果有誰知道靠這種模式獲得巨大獲利的 app ,請在評論中留言。

你的 app 能否持續提供最新資訊,或者任何形式的文件儲存服務?

如果你的 app 可以提供原創的新鮮資訊,頻率至少是每月一次,那麼你可以嘗試訂閱服務的獲利模式。像 Voxy 和 FarFaria 這樣的創業公司,他們成功地讓使用者分別訂閱語言學習內容和兒童書籍的服務。而且他們可以保證每週都能持續地提供內容。儘管蘋果公司對 app 內的自動收費要求很嚴格,但這樣持續的收入無疑是一個聚寶盆。(很多使用者甚至會忘記或懶得去把自動付費的功能關掉,儘管他們都不再使用該 app 了。)
有時蘋果公司也會允許開發者對不是持續提供內容的服務自動收費。對大多數非雜誌類的 app ,他們依賴於 app 外的訂閱(比如在他們的網站上或通過郵件訂閱),這讓蘋果很苦惱。比如 Dropbox、Netflix 和 Audible.com 就是這樣的。
無論上述你對五個問題的答案如何,關鍵的是嘗試不同的模式來發現哪種是最好,然後再透過最優化知道你能達到多好。Brainscape 開發了自己的A/B 測試系統,讓 app 在啟動時決定它的收費模式,並且會把銷售的數據發送回來。當然你也可以讓 app 先使用某種收費模式,然後在 app 更新的時候變成另外一種,透過這樣的轉換發現更高的收入使用者比。

重要的是你要不斷去嘗試,最「完美」的模式可以讓你從每位使用者獲得的收入增加10 倍,但只有不斷地試驗才能知道這是怎樣的模式。

2013年10月28日 星期一

20121129 AdMob合作夥伴日聽後心得

http://fstoke.me/blog/?p=3387

昨天去參加了Google辦的AdMob合作夥伴日活動,去了台北101 77樓的Google Office參觀了一下(圖為Google Office的員工交誼廳,左邊架子上有擺了一些員工專用的免費零食。不過比我想像中的少很多就是 ~”~。我覺得至少應該要有多力多滋和Expresso咖啡機才對啊~ :Q)。
這次的活動主要有5個主題:
1. App 流量價值最大化
2. App 推廣營收之道
3. Android 最新設計和技術趨勢
4. 兩岸三地成功開發者經驗分享
5. Q&A

前三個topic內容大體上來說還好,絕大部份跟AdMob有關的東西大概早就都知道了。比較特別的是AdMob最近推出了一個CPA的廣告計價方式,也就是以”App實際下載量“作 為計價方式而不是以”點擊數”。另外AdMob還有提供某地區的全天24hr所有平台上的廣告曝光(稱為Blast),效果應該會很不錯,不過我想費用一 定很高(幾百萬跑不掉?)…。Android 最新設計和技術趨勢的部份,因為是用視訊,而且講者是位香港的Google工程師,講話似乎不太清楚常結巴,而且我也沒啥興趣了解Android有更新了 什麼東東,所以就不寫了。
比較值得聽的部份是topic 4的獨立開發者經驗談,邀請了兩位台灣的Android工具類App開發者(Sam, Kenny)和一位大陸的遊戲類Android/iOS App開發者(宋偉)。其中有兩位開發者(Sam, 宋偉)都是主打歐美市場,App已經累計有一千萬次的下載。Kenny則是做本土工具App為主。)
其中Sam講到獨立開發者如何獲利的部份(偏工具類App):
1. Freeium 比 Free + Permium 使用者轉換率來的好。也就是Free App + In App Purchase升級功能,會比同時上架一套Free App和一套付費App在App Store上效果來的好。

2. 開發模式: 先以免費App上架,等過一段時間觀察App的使用量有持續增長而且量很大的時候,再考慮加廣告和IAP
(另: 加IAP可能會被使用者罵死要錢,Sam: 不理他們)

3. 開發工具類App一定要做”有人用的東西”(有使用者需求,每天都會想打開來用的),不要自己天馬行空做的很爽,但做出來沒人用(像我的F.Surface這個App就是個失敗的例子 =.=)
4. App開發者儘量不要只做local市場 -> 沒前途 (台灣市場的營收比例,大概只佔全世界的1%,為什麼要死守台灣市場?)
5. 獨立開發者不要撒錢在廣告上 -> 因為資源(錢)不夠多,成效不夠! 不如不要做… 靠口碑行銷比較重要 (我曾經親自下海過…我也這麼覺得)
6. 利用廣告中介SDK來播放廣告(因為合適的廣告版位可能只有一個): AdWhirl or AdMob Mediation
(聽說Google現在比較有在更新AdMob Mediation,AdWhirl好像比較少管了… 我目前是都用AdWhirl)

7. App加入廣告心法:
a. 可以加廣告的地方就儘量加,但以不影響使用者操作和觀感為前題
b. 廣告出現的位置,儘量在使用者”聚焦”的地方,但不是容易會按錯的地方
c. 母雞帶小雞: 利用自家廣告幫自己其他的App增加曝光量,母雞指的是目前在排行榜上成績不錯、流量高的App,小雞指的是剛上架不久的新App

8. 和其他獨立開發者交換廣告曝光
宋偉開發經驗分享的部份(偏遊戲類App):
1. App Icon很重要: 歐美玩家喜歡色彩飽和度較高的設計,亞洲玩家喜歡色彩柔和的設計。好的App Icon真的非常非常非常非常重要,該大陸講者曾經實驗,同時放兩個不同感覺的icon上架測試(同樣的App),其中一個飽和度強的icon沒多久就衝 上前20名,另一個則石沉大海…

2. 善用Google Analytics追踨玩家的記錄(用Flurry應 該也行)。例如: 你的遊戲有100個關卡,你可以在每一個關卡過關時送出一個Google Analytics Event去記錄。然後在報表上觀看一個玩家大概都會玩到第幾關,如果在某一個關卡過後,event記錄明顯少了很多,這代表可能該關卡讓玩家卡住玩不下 去,挫折惑太重走了。你便可以針對該關卡的難度做些調整,想辦法讓曲線圖是平滑的。另外,也可以記錄玩家到達某個關卡時的遊戲金幣大概平均是多少,以便設 定道具的合理價格或IAP商品的價值。利用這個工具可以幫助App多留住20%左右的玩家。
3. 在合適的時機、合適的地方出現在廣告,可以幫助營收。例如: Game在loading時、結束關卡時…etc.
4. 建議做一個自己App的廣告系統,指的是可以動態更新自己的新上架App出現在已上架的App中(即App聯播),這樣對自己的App推廣有幫助
最後Q&A:
Q. 獨立開發者如何做App語言在地化?
A. Sam: 首先,千萬不要找翻譯社翻譯,因為你無法確認他們翻譯出來的文字是否是正確符合原意的,而且費用高很多。可以直接找熱心的當地使用者自願幫忙翻譯成該國家 的語言,效果通常不錯。可以等事成之後再給他們一筆小錢或者禮物當作回饋,但不要一開始就講到錢。(不過我覺得這比較適合文字數比較少的工具類App才能 這樣,如果是像遊戲裡有大量的文字要翻譯的話,應該比較難找到玩家幫你一次翻完)

Q. 對於App盜版的看法?
A. 三位開發者都異口同聲這樣說: 不要浪費時間去做反盜版的機制,因為沒有用。宋偉: 其實他們觀察到大陸那些亂七八糟的破解App網站,反而有幫助App曝光的功效。所以他們也懶得去管盜版的問題。(所以我也該感謝大陸這個破解網站? =.,=)

Q. App如何在大陸市場run?
A. 宋偉: 免費!! 大陸玩家幾乎不會下載要付費的App… (意思是,所以你大概只能走Free App + 廣告這條路)

Q. 如何在App剛上架時就得到大量的曝光?
A. 宋偉: 先以付費版上架,再限時免費
Sam: 什麼都不做,App品質好自然就會有人用 (開發者和Google官方都指出,Google Play新上架App的排名效應並沒有像Apple App Store那樣明顯。所以我想Android App應該是要累積比較多的正面評價,然後獲得Google官方的推薦比較重要,算是打長期戰。)
另: 利用上述的母雞帶小雞的作法


Q. AdMob的廣告eCPM似乎較低?
A. AdMob的客戶營運經理出來回答: 可能跟某些local的廣告商比,的確是比較低沒錯,但Google是個有品牌有誠信的公司,不會讓開發者拿不到錢,技術能力也就絕對沒問題。反之,就不 一定了。與其花很多時間去試廣告商,還不如把精力留在開發好的App上。AdMob會更努力爭取更多廣告主投入很多廣告預算。(我同意她的說法,目前我也 只有用iAD和AdMob兩個廣告商而已。iAD雖然fill rate比較低,但廣告收益(eCPM)較好,也絕對拿的到錢。)

Android使用Google Analytics v2介绍2

http://www.karanter.com/chim/2012/394

1.说明
之前写过一篇关于1.x与2.x版本之前的差异对比,如果之前没有用过1.x版本可跳过。链接:Google Analytics SDK for Android v2使用介绍1
在使用之前需要一个ID,用来接受你的应用数据。地址:https://www.google.com/analytics/web/

2.使用方法
2.1 首先需要如下权限,用来上传数据:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
2.2 在Activity的onstart和onStop中分别添加如下代码:
 @Override
public void onStart() {
super.onStart();
... // The rest of your onStart() code.
EasyTracker.getInstance().activityStart(this); // Add this method.
}
@Override
public void onStop() {
super.onStop();
… // The rest of your onStop() code.
EasyTracker.getInstance().activityStop(this); // Add this method.
}

2.3 在res/values目录下创建analytics.xml文件并加入类似下面的代码,EasyTracker提供了多个配置参数,可以按自己的需求来添加,所有参数地址为:analytics.xml parameters reference
<?xml version="1.0" encoding="utf-8" ?>

<resources xmlns:tools="https://schemas.android.com/tools"
tools:ignore="TypographyDashes">
  <!--Replace placeholder ID with your tracking ID-->
  <string name="ga_trackingId">UA-XXXX-Y</string>

  <!--Enable Activity tracking-->
  <bool name="ga_autoActivityTracking">true</bool>

  <!--Enable automatic exception tracking-->
  <bool name="ga_reportUncaughtExceptions">true</bool>
</resources>
3 追踪方法详解
3.1Dispatching 统计数据的发送
//1.隔一段时间自动发送
//1.1EasyTracker 添加<bool name="ga_autoActivityTracking">true</bool> 不设置默认为30
//1.2代码中设置 GAServiceManager.getInstance().setDispatchPeriod(60);
//2.手动发送: GAServiceManager.getInstance().dispatch();
3.2 获取Tracker实例
3.2.1.使用GoogleAnalytics获得Tracker的实例
GoogleAnalytics myInstance = GoogleAnalytics.getInstance(context);
//下面方法有两种情况
//1。如果实例中没有传入TrakerId的Tracker,下面方法会创建一个
//2。如果已经有对应TrakerId的Tracker,下面会取出改实例
Tracker tracker =
myInstance.getTracker("UA-36603750-2");//参数为申请到的TrakerId
3.2.2.使用EasyTracker获得Tracker的实例
EasyTracker.getInstance().setContext(context); // 在使用EasyTracker时候要小心,如果之前没有设置context下面代码会出异常
EasyTracker.getTracker(); // 确保analytics已经设置了ID
3.2.3.获取Default Tracker
GoogleAnalytics myInstance = GoogleAnalytics.getInstance(context);
Tracker tracker = myInstance.getDefaultTracker();
//设置Default Tracker
Tracker newDefaultTracker = myInstance.getTracker("trackingId");
myInstance.setDefaultTracker(newDefaultTracker);
End Default
3.2.4.说明:如果非EasyTracker实例,不使用EasyTracker时
//1.如果要设置debug模式
myInstance.setDebug(true);
//2.如果想在程序里面某一段时间内停用或开启所有的事件最终可以使用下面代码来设置
myInstance.setAppOptOut(true);//or false
//下面代码是来处理,设置之后的回调
myInstance.requestAppOptOut(new AppOptOutCallback() {
@Override
public void reportAppOptOut(boolean optOut) {
if(optOut){

}
}
});
//3.如果要设置AppVersion,设置其他的属性,类似下面
tracker.setAppVersion("your app version string");
//End 说明
}
3.3 事件追踪
//category 事件类别, action 时间动作, label 事件标签, value 事件价值
mTracker.trackEvent(category, action, label, value); // 通过任意的tracker来追踪
3.4 界面追踪
/**
* Screen 1.使用EasyTracker 1.1可以再analytics.xml设置<bool name="ga_autoActivityTracking">true</bool>来自动追踪
* 1.2或者使用EasyTracker.getInstance().activityStart/activityStop来追踪。
* 保证数据的完整性使用1.2更好 2.手动追踪 在onStart中使用如下方法,一般用来追踪Fragment与其他非Activity界面
*/

mTracker..trackView(screenName);
3.5 社交
/**
* 追踪程序中社交相关操作
* @param network
* 社交网络的名称例如Google+
* @param action
* 操作,例如分享,转发
* @param target
* 可选参数,目标网址
*/

mTracker.trackSocial(network, action, target);
3.6 耗时追踪
/**
* 最终耗时,比如加载图片等资源耗时
*
* @param category
* 事件的类别
* @param name
* 事件的名称,可选参数
* @param label
* 事件的标签,可选参数
* @param time
* 事件耗时
*/

mTracker..trackTiming(category, time, name, label);
3.7 来源追踪
// Campain Tracking
1.Google Play Campain : set receiver in mainfest 用来追踪程序从Google Play安装的信息

        <receiver
           android:name="com.google.analytics.tracking.android.AnalyticsReceiver"
           android:exported="true" >
            <intent-filter>
                <action android:name="com.android.vending.INSTALL_REFERRER" />
            </intent-filter>
        </receiver>
2.Genaral Campaing Tracking 追踪其他应用或链接打开应用的数据,来源必须把campaign数据放在intent里面
/**
*
* @param context
* @param campaign
 字符串类似utm_campaign=general_campaign_test& utm_source=general_campaign_intsig&utm_medium=cpc&utm_term=general_campaign_intent_test&utm_content=ad_variation1
*/

mTracker..setCampaign(campaign);

<strong>3 </strong>Referrer Tracking 追踪其他应用或链接打开应用的数据,来源必须把campaign数据放在intent里面
/**
*
* @param context
* @param referrer
* 功能和上面类似,不过参数没有上面严格,任意字符串都可以,不过最好指定,或者只用上面,不然可能会拿到很多无用的数据
*/

mTracker.setReferrer(referrer);
3.8 Crashes and Exceptions 用来追踪程序的异常和崩溃数据,ga2.x增加功能
//1.追踪已经捕获的异常,在catch的里面用myTracker.trackException(e.getMessage, false); 第一个参数是需要提交的异常信息,第二个参数是否是致命错误
//2.追踪程序中未捕获的异常:
// 2.1 如果使用的是EastTracker可以在analytics.xml加上<bool name="ga_reportUncaughtExceptions">true</bool>;该值不设置默认为false
// 2.2 使用ExceptionReporter,使用方法如下:
UncaughtExceptionHandler myHandler = new ExceptionReporter(tracker, //Tracker的实例
GoogleAnalytics.getInstance(context), //GoogleAnalytics实例
Thread.getDefaultUncaughtExceptionHandler()); //uncaught exception handler
Thread.setDefaultUncaughtExceptionHandler(myHandler);
// 结合2.2中如果要获得更多有用的异常等信息,可以通过自定义一个解析类继承ExcaptionParser,并把其实例设置到当前使用的Tracker中去如下:
ExceptionParser parser = new myParser(context);
myTracker.setExceptionParser(parser);
3.9 Ecommerce Track 用来追踪程序里面的交易数据
// 步骤:
// 1.构建交易事务对象
// 2.构建交易条目对象,并添加到交易事务对象中
// 3.调用trackTransaction来提交追踪数据
/**
* 确保在交易完成时候提交这样的数据,不然会造成统计数据不准确,比如有一些未完成的订单混在已支付的订单中
*/

public void onPurchaseCompleted() {
//1 创建交易事务
Transaction myTrans = new Transaction.Builder("0_123456", // (String)
// 交易事务ID,唯一
(long) (2.16 * 1000000)) // (long) 订单总额
.setAffiliation("In-App Store") // (String) 订单来源
.setTotalTaxInMicros((long) (0.17 * 1000000)) // (long) 订单税率
.setShippingCostInMicros(0) // (long) 发运货用去金额
.build();

//2 添加交易条目
myTrans.addItem(new Item.Builder("L_789", // (String) 商品的 SKU
"Level Pack: Space", // (String) 商品名称
(long) (1.99 * 1000000), // (long) 商品价格
(long) 1) // (long)商品数量
.setProductCategory("Game expansions") // (String)商品类别
.build());

//3 提交数据
Tracker myTracker = getTracker();// 获取tracker引用.
myTracker.trackTransaction(myTrans); // 提交交易数据
}
// end Ecommerce Track
3.10 Session用户会话追踪,比如用户切换界面,登入登出等
//1使用EasyTracker自动会话追踪:<integer name="ga_sessionTimeout">30</integer>
//2大多数的时候需要手动追踪,比如用户等登入
myTracker.setStartSession(true);
myTracker.trackEcent("app_flow", "sign_in", "", null);

3.11 另外还有
//Custom Dimensions & Metrics 为下一个事件自定义参数,比如点击事件,screen事件等,自定义参数

2013年10月7日 星期一

Android OpenGL ES 开发中的Buffer使用

http://www.imobilebbs.com/wordpress/archives/1706

在前面介绍Android OpenGL ES简明开发教程 说过为了提高性能,通常将顶点,颜色等值存放在java.nio 包中定义的Buffer类中。

ByteBuffer vbb
 = ByteBuffer.allocateDirect(vertices.length * 4);
vbb.order(ByteOrder.nativeOrder());
vertexBuffer = vbb.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);


本篇简要介绍一下java.nio 包中各种Buffer的定义和用法,也为后面详细介绍Android OpenGL ES开发做点知识上的准备。
使用Java开发文件读取时经常用到的一个包是java.io ,包内定义了各种流Stream  类,将文件或是Stream看作一个byte数组,这种方法一般无法指定字节顺序(不同的计算机系统Byte order 可能不同),操作如“在文件偏移10个字节出读取一个四字节Little-endian整数“在使用Stream方式读取文件时就不十分方便。此外,Stream 方法是按一个字节一个字节方式处理的,读写性能上不是很好。
java.nio (这里的N代表New 新的IO包) 解决了上述java.io 包的这些局限,java.io 包包含了Buffer,Channel, charset 等,但OpenGL ES 只用到Buffer,Buffer具有以下特点:
  • 允许以内存缓冲区(buffer)的方式来管理一个Buffer 数组,可以整块整块的读写内存区域,并可以指定Byte order.(大头或是小头)。
  • 提供了在指定位置读写各种基本数据类型的简便方法如 putInt ,putLong等。
  • 可以充分利用JVM提供的各种优化方法以达到和使用Native Code类型的读写性能。
  • 可以直接从OS的内存分配空间,这部分空间可以不受Java 的Garbage collector控制。称为Direct buffer.
简单的说使用java.nio 中的Buffer类提供了更高的访问性能,这也是为什么OpenGL ES使用Buffer类的主要原因。
Buffer定义了三个状态变量:position, limit, capacity
  • Capacity: Buffer的容量,表示可以存放的最大字节数,内存分配之后其值保持不变。
  • Position: 类似于文件指针,表示下一个可以读写的字节的缺省位置,可以使用函数来重新设置当前的Position.
  • Limit: 可以控制当前可以读写的区域,你只可以读写从[0,limit-1]范围内的数组空间,读写超过这个范围将导致抛出异常。
通常情况下这些状态变量有buffer自动管理,但也可以使用方法来重新设置这些变量。
比如: public final Bufferlimit(int newLimit) 重新设置limit
public final Bufferposition(int newPosition) 重新设置position
此外reset ,rewind mark ,clear 等在清空Buffer或是恢复Mark位置时也可修改limit ,position 的值。
get()方法
在ByteBuffer类中提供了四种get()方法用于读取:
1. byte get(); 2. ByteBuffer get( byte dst[] ); 3. ByteBuffer get( byte dst[], int offset, int length ); 4. byte get( int index );
put() 方法
在ByteBuffer中定义了五种put()方法用于写入:
1. ByteBuffer put( byte b ); 2. ByteBuffer put( byte src[] ); 3. ByteBuffer put( byte src[], int offset, int length ); 4. ByteBuffer put( ByteBuffer src ); 5. ByteBuffer put( int index, byte b );
此外还定义了多种基本数据类型的读取写入方法:
· getByte() · getChar() · getShort() · getInt() · getLong() · getFloat() · getDouble() · putByte() · putChar() · putShort() · putInt() · putLong() · putFloat() · putDouble()
java.nio 包中定义了基类Buffer ,还定义了和各种基本数据类型特定的Buffer类型ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer和ShortBuffer.其中ByteBuffer是其它类型的基础,因为分配内存是通过ByteBuffer的allocate来进行的,然后通过 ByteBuffer 的asXXXBuffer()转为其它类型:
可以使用下面三个方法来创建一个新的ByteBuffer:
public static ByteBuffer allocate(int capacity) public static ByteBuffer allocateDirect(int capacity) public static ByteBuffer wrap(byte[] array)
其中ByteBufferallocateDirect直接从OS分配内存,不受GC的限制,wrap使用已分配的数组作为buffer管理。
然后通过asXXXBuffer 转为其它类型的Buffer:
public abstract CharBuffer      asCharBuffer() public abstract DoubleBuffer     asDoubleBuffer() public abstract FloatBuffer     asFloatBuffer() public abstract IntBuffer        asIntBuffer() public abstract LongBuffer        asLongBuffer() public abstract ShortBuffer        asShortBuffer()
以上介绍了Android OpenGL ES相关的Buffer的使用方法,将在不久的将来详细介绍OpenGL ES开发指南。

Android OpenGL ES 简明开发教程小结

http://www.imobilebbs.com/wordpress/archives/1583

前面简单介绍了OpenGL ES的开发:
  1. Android OpenGL ES 简明开发教程一:概述
  2. Android OpenGL ES 简明开发教程二:构造OpenGL ES View
  3. Android OpenGL ES 简明开发教程三:3D绘图基本概念
  4. Android OpenGL ES 简明开发教程四:3D 坐标变换
  5. Android OpenGL ES 简明开发教程五:添加颜色
  6. Android OpenGL ES 简明开发教程六: 真正的3D图形
  7. Android OpenGL ES 简明开发教程七:材质渲染
和2D图形相比,3D绘图要复杂的多,Android提供了OpenGL ES 3D 图形开发包,对应熟悉OpenGL开发的不会很难,但如果一直没有从事3D开发过,一时还不容易上手,因此暂时跳过Android  ApiDemos 中后OpenGL相关的例子,计划将在后面详细介绍Android OpenGL ES开发,之后补上这部分例子

Android OpenGL ES 简明开发教程七:材质渲染

http://www.imobilebbs.com/wordpress/archives/1571

前面讨论了如何给3D图形染色,更一般的情况是使用位图来给Mesh上色(渲染材质)。主要步骤如下:
创建Bitmap对象
使用材质渲染,首先需要构造用来渲染的Bitmap对象,Bitmap对象可以从资源文件中读取或是从网络下载或是使用代码构造。为简单起见,本例从资源中读取:
Bitmap bitmap = BitmapFactory.decodeResource(contect.getResources(),
 R.drawable.icon);


要注意的是,有些设备对使用的Bitmap的大小有要求,要求Bitmap的宽度和长度为2的几次幂(1,2,4,8,16,32,64.。。。),如果使用不和要求的Bitmap来渲染,可能只会显示白色。
创建材质(Generating a texture)
下一步使用OpenGL库创建一个材质(Texture),首先是获取一个Texture Id。
// Create an int array with the number of textures we want,
// in this case 1.
int[] textures = new int[1];
// Tell OpenGL to generate textures.
gl.glGenTextures(1, textures, 0);


textures中存放了创建的Texture ID,使用同样的Texture Id ,也可以来删除一个Texture:
// Delete a texture.
gl.glDeleteTextures(1, textures, 0)


有了Texture Id之后,就可以通知OpenGL库使用这个Texture:
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

设置Texture参数glTexParameter
下一步需要给Texture填充设置参数,用来渲染的Texture可能比要渲染的区域大或者小,这是需要设置Texture需要放大或是缩小时OpenGL的模式:
// Scale up if the texture if smaller.
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
 GL10.GL_TEXTURE_MAG_FILTER,
 GL10.GL_LINEAR);

// scale linearly when image smalled than texture
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
 GL10.GL_TEXTURE_MIN_FILTER,
 GL10.GL_LINEAR);


常用的两种模式为GL10.GL_LINEAR和GL10.GL_NEAREST。
需要比较清晰的图像使用GL10.GL_NEAREST:
而使用GL10.GL_LINEAR则会得到一个较模糊的图像:
UV Mapping
下一步要告知OpenGL库如何将Bitmap的像素映射到Mesh上。这可以分为两步来完成:
定义UV坐标
UV Mapping指将Bitmap的像素映射到Mesh上的顶点。UV坐标定义为左上角(0,0),右下角(1,1)(因为使用的2D Texture),下图坐标显示了UV坐标,右边为我们需要染色的平面的顶点顺序:
为了能正确的匹配,需要把UV坐标中的(0,1)映射到顶点0,(1,1)映射到顶点2等等。
float textureCoordinates[] = {0.0f, 1.0f,
 1.0f, 1.0f,
 0.0f, 0.0f,
 1.0f, 0.0f };


如果使用如下坐标定义:

float textureCoordinates[] = {0.0f, 0.5f,
 0.5f, 0.5f,
 0.0f, 0.0f,
 0.5f, 0.0f };


Texture匹配到Plane的左上角部分。
float textureCoordinates[] = {0.0f, 2.0f,
 2.0f, 2.0f,
 0.0f, 0.0f,
 2.0f, 0.0f };

将使用一些不存在的Texture去渲染平面(UV坐标为0,0-1,1 而 (0,0)-(2,2)定义超过UV定义的大小),这时需要告诉OpenGL库如何去渲染这些不存在的Texture部分。
有两种设置
  • GL_REPEAT 重复Texture。
  • GL_CLAMP_TO_EDGE 只靠边线绘制一次。
下面有四种不同组合:
本例使用如下配置:
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
 GL10.GL_TEXTURE_WRAP_S,
 GL10.GL_REPEAT);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
 GL10.GL_TEXTURE_WRAP_T,
 GL10.GL_REPEAT);

然后是将Bitmap资源和Texture绑定起来:
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);


使用Texture
为了能够使用上面定义的Texture,需要创建一Buffer来存储UV坐标:
FloatBuffer byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(textureCoordinates);
textureBuffer.position(0);


渲染
// Telling OpenGL to enable textures.
gl.glEnable(GL10.GL_TEXTURE_2D);
// Tell OpenGL where our texture is located.
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
// Tell OpenGL to enable the use of UV coordinates.
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
// Telling OpenGL where our UV coordinates are.
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

// ... here goes the rendering of the mesh ...

// Disable the use of UV coordinates.
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
// Disable the use of textures.
gl.glDisable(GL10.GL_TEXTURE_2D);


本例代码是在一个平面上(SimplePlane)下使用Texture来渲染,首先是修改Mesh基类,使它能够支持定义UV 坐标:
// Our UV texture buffer.
private FloatBuffer mTextureBuffer;

/**
 * Set the texture coordinates.
 *
 * @param textureCoords
 */
protected void setTextureCoordinates(float[] textureCoords) {
 // float is 4 bytes, therefore we multiply the number if
 // vertices with 4.
 ByteBuffer byteBuf = ByteBuffer.allocateDirect(
 textureCoords.length * 4);
 byteBuf.order(ByteOrder.nativeOrder());
 mTextureBuffer = byteBuf.asFloatBuffer();
 mTextureBuffer.put(textureCoords);
 mTextureBuffer.position(0);
}
并添加设置Bitmap和创建Texture的方法:
// Our texture id.
private int mTextureId = -1;

// The bitmap we want to load as a texture.
private Bitmap mBitmap;

/**
 * Set the bitmap to load into a texture.
 *
 * @param bitmap
 */
public void loadBitmap(Bitmap bitmap) {
 this.mBitmap = bitmap;
 mShouldLoadTexture = true;
}

/**
 * Loads the texture.
 *
 * @param gl
 */
private void loadGLTexture(GL10 gl) {
 // Generate one texture pointer...
 int[] textures = new int[1];
 gl.glGenTextures(1, textures, 0);
 mTextureId = textures[0];

 // ...and bind it to our array
 gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureId);

 // Create Nearest Filtered Texture
 gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
 GL10.GL_LINEAR);
 gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
 GL10.GL_LINEAR);

 // Different possible texture parameters, e.g. GL10.GL_CLAMP_TO_EDGE
 gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
 GL10.GL_CLAMP_TO_EDGE);
 gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
 GL10.GL_REPEAT);

 // Use the Android GLUtils to specify a two-dimensional texture image
 // from our bitmap
 GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmap, 0);
}
最后修改draw方法来渲染材质:
// Indicates if we need to load the texture.
private boolean mShouldLoadTexture = false;

/**
 * Render the mesh.
 *
 * @param gl
 *            the OpenGL context to render to.
 */
public void draw(GL10 gl) {
 ...

 // Smooth color
 if (mColorBuffer != null) {
 // Enable the color array buffer to be used during rendering.
 gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
 gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
 }

 if (mShouldLoadTexture) {
 loadGLTexture(gl);
 mShouldLoadTexture = false;
 }
 if (mTextureId != -1 && mTextureBuffer != null) {
 gl.glEnable(GL10.GL_TEXTURE_2D);
 // Enable the texture state
 gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

 // Point to our buffers
 gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mTextureBuffer);
 gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureId);
 }

 gl.glTranslatef(x, y, z);

 ...

 // Point out the where the color buffer is.
 gl.glDrawElements(GL10.GL_TRIANGLES, mNumOfIndices,
 GL10.GL_UNSIGNED_SHORT, mIndicesBuffer);

 ...

 if (mTextureId != -1 && mTextureBuffer != null) {
 gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
 }

 ...

}


本例使用的SimplePlane定义如下:
package se.jayway.opengl.tutorial.mesh;

/**
 * SimplePlane is a setup class for Mesh that creates a plane mesh.
 *
 * @author Per-Erik Bergman (per-erik.bergman@jayway.com)
 *
 */
public class SimplePlane extends Mesh {
 /**
 * Create a plane with a default with and height of 1 unit.
 */
 public SimplePlane() {
 this(1, 1);
 }

 /**
 * Create a plane.
 *
 * @param width
 *            the width of the plane.
 * @param height
 *            the height of the plane.
 */
 public SimplePlane(float width, float height) {
 // Mapping coordinates for the vertices
 float textureCoordinates[] = { 0.0f, 2.0f, //
 2.0f, 2.0f, //
 0.0f, 0.0f, //
 2.0f, 0.0f, //
 };

 short[] indices = new short[] { 0, 1, 2, 1, 3, 2 };

 float[] vertices = new float[] { -0.5f, -0.5f, 0.0f,
 0.5f, -0.5f, 0.0f,
 -0.5f,  0.5f, 0.0f,
 0.5f, 0.5f, 0.0f };

 setIndices(indices);
 setVertices(vertices);
 setTextureCoordinates(textureCoordinates);
 }
}
本例示例代码下载 ,到本篇为止介绍了OpenGL ES开发的基本方法,更详细的教程将在以后发布,后面先回到Android ApiDemos中OpenGL ES的示例。

Android OpenGL ES 简明开发教程六: 真正的3D图形

http://www.imobilebbs.com/wordpress/archives/1554

前面的例子尽管使用了OpenGL ES 3D图形库,但绘制的还是二维图形(平面上的正方形)。Mesh(网格,三角面)是构成空间形体的基本元素,前面的正方形也是有两个Mesh构成的。本篇将介绍使用Mesh构成四面体,椎体等基本空间形体。
Design设计
在使用OpenGL 框架时一个好的设计原则是使用“Composite Pattern”,本篇采用如下设计:
Mesh
首先定义一个基类 Mesh,所有空间形体最基本的构成元素为Mesh(三角形网格) ,其基本定义如下:

public class Mesh {
 // Our vertex buffer.
 private FloatBuffer verticesBuffer = null;

 // Our index buffer.
 private ShortBuffer indicesBuffer = null;

 // The number of indices.
 private int numOfIndices = -1;

 // Flat Color
 private float[] rgba
 = new float[] { 1.0f, 1.0f, 1.0f, 1.0f };

 // Smooth Colors
 private FloatBuffer colorBuffer = null;

 // Translate params.
 public float x = 0;

 public float y = 0;

 public float z = 0;

 // Rotate params.
 public float rx = 0;

 public float ry = 0;

 public float rz = 0;

 public void draw(GL10 gl) {
 // Counter-clockwise winding.
 gl.glFrontFace(GL10.GL_CCW);
 // Enable face culling.
 gl.glEnable(GL10.GL_CULL_FACE);
 // What faces to remove with the face culling.
 gl.glCullFace(GL10.GL_BACK);
 // Enabled the vertices buffer for writing and
 //to be used during
 // rendering.
 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
 // Specifies the location and data format
 //of an array of vertex
 // coordinates to use when rendering.
 gl.glVertexPointer(3, GL10.GL_FLOAT, 0, verticesBuffer);
 // Set flat color
 gl.glColor4f(rgba[0], rgba[1], rgba[2], rgba[3]);
 // Smooth color
 if (colorBuffer != null) {
 // Enable the color array buffer to be
 //used during rendering.
 gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
 gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);
 }

 gl.glTranslatef(x, y, z);
 gl.glRotatef(rx, 1, 0, 0);
 gl.glRotatef(ry, 0, 1, 0);
 gl.glRotatef(rz, 0, 0, 1);

 // Point out the where the color buffer is.
 gl.glDrawElements(GL10.GL_TRIANGLES, numOfIndices,
 GL10.GL_UNSIGNED_SHORT, indicesBuffer);
 // Disable the vertices buffer.
 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
 // Disable face culling.
 gl.glDisable(GL10.GL_CULL_FACE);
 }

 protected void setVertices(float[] vertices) {
 // a float is 4 bytes, therefore
 //we multiply the number if
 // vertices with 4.
 ByteBuffer vbb
 = ByteBuffer.allocateDirect(vertices.length * 4);
 vbb.order(ByteOrder.nativeOrder());
 verticesBuffer = vbb.asFloatBuffer();
 verticesBuffer.put(vertices);
 verticesBuffer.position(0);
 }

 protected void setIndices(short[] indices) {
 // short is 2 bytes, therefore we multiply
 //the number if
 // vertices with 2.
 ByteBuffer ibb
 = ByteBuffer.allocateDirect(indices.length * 2);
 ibb.order(ByteOrder.nativeOrder());
 indicesBuffer = ibb.asShortBuffer();
 indicesBuffer.put(indices);
 indicesBuffer.position(0);
 numOfIndices = indices.length;
 }

 protected void setColor(float red, float green,
 float blue, float alpha) {
 // Setting the flat color.
 rgba[0] = red;
 rgba[1] = green;
 rgba[2] = blue;
 rgba[3] = alpha;
 }

 protected void setColors(float[] colors) {
 // float has 4 bytes.
 ByteBuffer cbb
 = ByteBuffer.allocateDirect(colors.length * 4);
 cbb.order(ByteOrder.nativeOrder());
 colorBuffer = cbb.asFloatBuffer();
 colorBuffer.put(colors);
 colorBuffer.position(0);
 }
}

  • setVertices 允许子类重新定义顶点坐标。
  • setIndices 允许子类重新定义顶点的顺序。
  • setColor /setColors允许子类重新定义颜色。
  • x,y,z 定义了平移变换的参数。
  • rx,ry,rz 定义旋转变换的参数。
Plane
有了Mesh定义之后,再来构造Plane,plane可以有宽度,高度和深度,宽度定义为沿X轴方向的长度,深度定义为沿Z轴方向长度,高度为Y轴方向。
Segments为形体宽度,高度,深度可以分成的份数。 Segments在构造一个非均匀分布的Surface特别有用,比如在一个游戏场景中,构造地貌,使的Z轴的值随机分布在-0.1到0.1之间,然后给它渲染好看的材质就可以造成地图凹凸不平的效果。
上面图形中Segments为一正方形,但在OpenGL中我们需要使用三角形,所有需要将Segments分成两个三角形。为Plane 定义两个构造函数:
// Let you decide the size of the plane but still only one segment. public Plane(float width, float height)
// For alla your settings. public Plane(float width, float height, int widthSegments, int heightSegments)
比如构造一个1 unit 宽和 1 unit高,并分成4个Segments,使用图形表示如下:
左边的图显示了segments ,右边的图为需要创建的Face(三角形)。
Plane类的定义如下:
public class Plane extends Mesh {
 public Plane() {
 this(1, 1, 1, 1);
 }

 public Plane(float width, float height) {
 this(width, height, 1, 1);
 }

 public Plane(float width, float height, int widthSegments,
 int heightSegments) {
 float[] vertices
 = new float[(widthSegments + 1)
 * (heightSegments + 1) * 3];
 short[] indices
 = new short[(widthSegments + 1)
 * (heightSegments + 1)* 6];

 float xOffset = width / -2;
 float yOffset = height / -2;
 float xWidth = width / (widthSegments);
 float yHeight = height / (heightSegments);
 int currentVertex = 0;
 int currentIndex = 0;
 short w = (short) (widthSegments + 1);
 for (int y = 0; y < heightSegments + 1; y++) {
 for (int x = 0; x < widthSegments + 1; x++) {
 vertices[currentVertex] = xOffset + x * xWidth;
 vertices[currentVertex + 1] = yOffset + y * yHeight;
 vertices[currentVertex + 2] = 0;
 currentVertex += 3;

 int n = y * (widthSegments + 1) + x;

 if (y < heightSegments && x < widthSegments) {
 // Face one
 indices[currentIndex] = (short) n;
 indices[currentIndex + 1] = (short) (n + 1);
 indices[currentIndex + 2] = (short) (n + w);
 // Face two
 indices[currentIndex + 3] = (short) (n + 1);
 indices[currentIndex + 4] = (short) (n + 1 + w);
 indices[currentIndex + 5] = (short) (n + 1 + w - 1);

 currentIndex += 6;
 }
 }
 }

 setIndices(indices);
 setVertices(vertices);
 }
}

Cube
下面来定义一个正方体(Cube),为简单起见,这个四面体只可以设置宽度,高度,和深度,没有和Plane一样提供Segments支持。
public class Cube extends Mesh {
 public Cube(float width, float height, float depth) {
 width  /= 2;
 height /= 2;
 depth  /= 2;

 float vertices[] = { -width, -height, -depth, // 0
 width, -height, -depth, // 1
 width,  height, -depth, // 2
 -width,  height, -depth, // 3
 -width, -height,  depth, // 4
 width, -height,  depth, // 5
 width,  height,  depth, // 6
 -width,  height,  depth, // 7
 };

 short indices[] = { 0, 4, 5,
 0, 5, 1,
 1, 5, 6,
 1, 6, 2,
 2, 6, 7,
 2, 7, 3,
 3, 7, 4,
 3, 4, 0,
 4, 7, 6,
 4, 6, 5,
 3, 0, 1,
 3, 1, 2, };

 setIndices(indices);
 setVertices(vertices);
 }
}

Group
Group可以用来管理多个空间几何形体,如果把Mesh比作Android的View ,Group可以看作Android的ViewGroup,Android的View的设计也是采用的“Composite Pattern”。
Group的主要功能是把针对Group的操作(如draw)分发到Group中的每个成员对应的操作(如draw)。
Group定义如下:
public class Group extends Mesh {
 private Vector<Mesh> children = new Vector<Mesh>();

 @Override
 public void draw(GL10 gl) {
 int size = children.size();
 for( int i = 0; i < size; i++)
 children.get(i).draw(gl);
 }

 /**
 * @param location
 * @param object
 * @see java.util.Vector#add(int, java.lang.Object)
 */
 public void add(int location, Mesh object) {
 children.add(location, object);
 }

 /**
 * @param object
 * @return
 * @see java.util.Vector#add(java.lang.Object)
 */
 public boolean add(Mesh object) {
 return children.add(object);
 }

 /**
 *
 * @see java.util.Vector#clear()
 */
 public void clear() {
 children.clear();
 }

 /**
 * @param location
 * @return
 * @see java.util.Vector#get(int)
 */
 public Mesh get(int location) {
 return children.get(location);
 }

 /**
 * @param location
 * @return
 * @see java.util.Vector#remove(int)
 */
 public Mesh remove(int location) {
 return children.remove(location);
 }

 /**
 * @param object
 * @return
 * @see java.util.Vector#remove(java.lang.Object)
 */
 public boolean remove(Object object) {
 return children.remove(object);
 }

 /**
 * @return
 * @see java.util.Vector#size()
 */
 public int size() {
 return children.size();
 }

}

其它建议
上面我们定义里Mesh, Plane, Cube等基本空间几何形体,对于构造复杂图形(如人物),可以预先创建一些通用的几何形体,如果在组合成较复杂的形体。除了上面的基本形体外,可以创建如Cone,Pryamid, Cylinder等基本形体以备后用。
本例示例代码下载,显示结果如下: