2018年12月31日 星期一

矩陣的排列順序(1)

矩陣的排列順序(1)

前言

  一個4*4矩陣是由16個float所組成,這不會有異議,但它的排列方式有兩種,常常會搞混,所以作這篇來記錄。

內容

  矩陣的兩種排列順序,如下圖
矩陣排列順序比較
如果仔細觀察可以發現這兩種排列可以透過Transpose的方式來互換,但不幸的是Direct3D與OpenGL的API只認其中一種順序,答案是"右邊",OpenGL2.0時使用的glUniformMatrix4fv()有個參數可以Transpose,所以嚴格來說沒限制順序,但Direct3D10以後開始使用ConstantBuffer與OpenGL3.1以後使用的UBO,這兩者都是直接透過記憶體將參數直接傳入GPU,沒辦法一個一個Transpose,所以才說繪圖API有認順序。

  這兩種順序在不考慮繪圖API的情形下沒有優劣之分,但由於繪圖API認定了其中一種排列方式,想想看,如果遊戲引擎採用"左邊"的排列,在Shader有matrix的變數的時候要如何寫?每次寫完後都Transpose嗎?還是每次將記憶體送到GPU時都將該變數Transpose?不論是哪一種都沒有"右邊"的排列好用,"右邊"的排列完全連改都不用改,還要想要選哪一種排列嗎?


2018年12月24日 星期一

在Atom中使用gdb

在Atom中使用gdb

前言

  在Ubuntu裡要Debug C++程式相當麻煩,Linux的C++編譯器有GCC或G++,Debugger則是GDB,GDB的教學可以參照GDB 基本操作| 教學,但用Console來控制Debugger實在是不怎麼方便,如果能像Vistual studio的方式來Debug不是很好嗎?在Ubuntu裡我用的文字編輯器是Atom,查了一下有外掛可以裝,在此把使用過程做個紀錄。

內容
  首先要先安裝兩個外掛,如下圖
安裝相關外掛
安裝"dbg"與"dbg-gdb"這兩個外掛。接著是重新編譯程式,範例如下
g++ main.cpp -o main.exe

g++ -g main.cpp -o main.exe

第一行是原本的編譯命令,請加上"-g",這樣gdb才能順利地使用break point。

  如何使用呢?在範例輸出的"main.exe"的binary檔按下右鍵,如下圖
選擇要Debug的Binary檔
按下"Debug this File"後,如下圖
設置Break pont後開始Debug
在開始Debug前,請先在需要設置break point的檔案設置,設置的方法很簡單,在需要設置的那一行按下"F9"就會有如圖的"紅點"在該行的左邊,全部設置完後,按下右下角的"Debug"就可以開始Debug,如下圖
Debug的狀況
可以在右邊看到變數狀況,左邊有控制Debug流程的UI,下方的則是Console的輸出結果,用起來就像Vistual studio一樣相當簡單。

參考資料

GDB 基本操作| 教學

2018年12月17日 星期一

初探OpenSLES

初探OpenSLES

前言

  在Android平台系統提供的音效API為OpenSLES,不過可以在GitHub找到OpenAL in Android,在評估過後,我還是決定使用系統提供的,在此把學習過程做個紀錄。

內容

  在評估在Android時到底要用OpenAL還是OpenSLES時,由於擔心OpenAL的部分不是官方所提供的,如果有問題很難得到支援,所以就選了OpenSLES,但OpenSLES的用法和OpenAL或XAudio2相近嗎?是的,至少在Stream play的流程是一樣的,整合起來不會有太大的問題。

  OpenSLES的範例並不多,目前的參考範例為NDK的範例。在開始Stream play前,和OpenAL與XAudio2一樣要初始化,具體的範例如下
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
int main()
{
  //
  //...  
  //
  SLObjectItf engineObject;
  SLEngineItf engineInterface;
  SLObjectItf outputMixObject;
  SLEngineOption engineOption[]=
  {
    {
      (SLuint32)SL_ENGINEOPTION_THREADSAFE,
      (SLuint32)SL_BOOLEAN_TRUE
    }
  };
  SLresult res=slCreateEngine(&engineObject,1,engineOption,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->Realize(engineObject,SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineInterface)->CreateOutputMix(engineInterface,&outputMixObject,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  //...
  //
  (*outputMixObject)->Destroy(outputMixObject);
  (*engineObject)->Destroy(engineObject);
  return 0;
}

先來說說OpenSLES的API使用習慣,SLObjectItf的物件在經過Create後,一定要Realize()才會生效,這點一定要注意!釋放的部分就比較簡單,直接喚起Destroy()即可,接著是介面物件,可以想像是物件的Public funciton,不過必須透過GetInterface()來取得,介面物件並不需要做釋放的動作。EngineObject的Create是透過slCreateEngine()來建立,建立後喚起Realize(),接著取得該物件的介面,也就是EngineInterface,接著透過engine interface來Create OutputMixObject,一樣,OutputMixObject在Create後馬上喚起Realize(),這樣就初始化完成了。釋放的部分相當簡單,對個別物件喚起Destroy()即可。

  接著來說明Stream play的實現,先看看範例
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
bool g_IsPlaying=false;
void PlayerPlayCallback(SLPlayItf caller,void* pContext, SLuint32 iEvent)
{
  // play finish
  if(iEvent==SL_PLAYEVENT_HEADATEND)
  {
    //On play end...
    g_IsPlaying=false;
  }
}
void PlayerBufferQueueCallBack(SLAndroidSimpleBufferQueueItf playerBufferQueueInterface,void* pContext)
{
  //
  //Enqueue next buffer...
}
int main()
{
  //
  //...  
  //
  SLObjectItf engineObject;
  SLEngineItf engineInterface;
  SLObjectItf outputMixObject;
  SLEngineOption engineOption[]=
  {
    {
      (SLuint32)SL_ENGINEOPTION_THREADSAFE,
      (SLuint32)SL_BOOLEAN_TRUE
    }
  };
  SLresult res=slCreateEngine(&engineObject,1,engineOption,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->Realize(engineObject,SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*engineInterface)->CreateOutputMix(engineInterface,&outputMixObject,0,NULL,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  //Follow data load from WAV file
  WAVEFORMATEX format;
  unsigned char* pWAVRawData;
  size_t wavRawDataSize;
  //
  SLObjectItf playerObject;
  SLPlayItf playerPlayInterface;
  SLAndroidSimpleBufferQueueItf playerBufferQueueInterface;
  SLDataLocator_AndroidSimpleBufferQueue locatorBufferQueue= 
  {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
  SLuint32 tagBitsPerSample=waveFormat.wBitsPerSample*waveFormat.nChannels;
  SLDataFormat_PCM formatPCM= 
  {
    SL_DATAFORMAT_PCM,
    waveFormat.nChannels,
    waveFormat.nSamplesPerSec*1000,
    tagBitsPerSample,
    tagBitsPerSample,
    SL_SPEAKER_FRONT_CENTER, 
    SL_BYTEORDER_LITTLEENDIAN
  };
  SLDataSource audioSource={&locatorBufferQueue,&formatPCM};
  SLDataLocator_OutputMix locatorOutmix={SL_DATALOCATOR_OUTPUTMIX,outputMixObject };
  SLDataSink audioSink={&locatorOutmix,NULL};
  SLInterfaceID interfaceIDList[]={SL_IID_BUFFERQUEUE,SL_IID_VOLUME, SL_IID_EFFECTSEND};
  SLboolean requiredList[] = {SL_BOOLEAN_TRUE,SL_BOOLEAN_TRUE,SL_BOOLEAN_TRUE};
  res=(*engineInterface)->CreateAudioPlayer(engineInterface,&playerObject,&audioSource,&audioSink,2,interfaceIDList,requiredList);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerObject)->Realize(playerObject,SL_BOOLEAN_FALSE);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerObject)->GetInterface(playerObject,SL_IID_PLAY,&playerPlayInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerPlayInterface)->RegisterCallBack(playerPlayInterface,PlayerPlayCallback,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerPlayInterface)->SetCallbackEventsMask(playerPlayInterface,SL_PLAYEVENT_HEADATEND);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerObject)->GetInterface(playerObject,SL_IID_BUFFERQUEUE,&playerBufferQueueInterface);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  res=(*playerBufferQueueInterface)->RegisterCallback(playerBufferQueueInterface,PlayerBufferQueueCallBack,NULL);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  res=(*playerBufferQueueInterface)->Enqueue(playerBufferQueueInterface,pWAVRawData,wavRawDataSize);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  g_IsPlaying=true;
  res=(*playerPlayInterface)->SetPlayState(playerPlayInterface,SL_PLAYSTATE_PLAYING);
  if(res!=SL_RESULT_SUCCESS)
    return 1;
  //
  while(g_IsPlaying)
    ::Sleeping(1)//sleep 1ms
  //
  (*playerObject)->Destroy(playerObject);
  (*outputMixObject)->Destroy(outputMixObject);
  (*engineObject)->Destroy(engineObject);
  return 0;
}

範例碼很長,但其實是因為OpenSLES的使用習慣造成看起來很長,和OpenAL與XAudio2一樣Create一個播放元件,OpenSLES的播放元件是"AudioPlayer",初始化的參數很多,要注意的只有SLDataFormat_PCM,裡面設定的是播放資料的格式,其餘的請照抄 ,AudioPlayer在Create後一樣要Realize(),接著是取得PlayerPlayInterface與PlayerBufferQueueInterface,PlayerPlayInterface控制的是播放與停止,還要注意RegisterCallBack()與SetCallbackEventsMask(),這兩個步驟,這兩個步驟是要在播放器播完聲音後喚起起一個CallBack,範例的話就是PlayerPlayCallback(),PlayerBufferQueueInterface控制的是Stream play的queue,和XAudio2一樣不需要自己Dequeue,但要透過RegisterCallback()來註冊CallBack,這個CallBack會在每處理完一個Buffer後喚起,在範例的話就是PlayerBufferQueueCallBack(),PlayerPlayCallback()與PlayerBufferQueueCallBack()都可以看到"pContext"的參數,也可以看到RegisterCallback()的最後一個參數,範例是都傳NULL,這個部分可以依據需要傳入物件的指標,在CallBack的時候再將pContext做轉型來做到傳遞物件,範例在要Equeue next buffer的部分是空的,請依據需要傳入物件指標來完成,Enqueue的部分可以用PlayerBufferQueueInterface來實現。

  整體來說,OpenSLES的程式碼會看起來很長,因為使用習慣的關係,在Stream play的部分,有一些麻煩,就是要規劃Enqueue next buffer的規格與規劃播放結束的處理。加上先前的初探XAudio2初探OpenAL來參考,就可以寫出整合這3套API的Stream play。

參考資料

OpenAL in Android
OpenSLES官網

相關文章

WAV檔案格式學習心得
初探XAudio2
初探OpenAL

2018年12月10日 星期一

初探OpenAL

初探OpenAL

前言

  在前篇初探XAudio2裡使用了XAudio2來播放聲音,這次來使用OpenAL來播放聲音,為什麼使用OpenAL呢?因為Linux裡沒有XAudio2可以使用,在Linux有一套名為ALSA的API,但查了一下授權條款是GPL,所以決定放棄這一套API,所以在Linux使用OpenAL來實現播放聲音,在此把研究過程做個紀錄。

內容
  在開始之前,請先到OpenAL官網下載SDK,請下載下圖所示選項
下載OpenAL SDK
下載後解壓安裝即可,這樣才有OpenAL的環境與C++的Header和Lib可以用。請在專案中新增Lib路徑,路徑為"(SDK directory)/libs",檔名為"OpenAL32.lib",include路徑為"(SDK directory)/include"。

  OpenAL的範例可以在"(SDK directory)/samples"裡找到,但範例有用framework,如果需要教學網站的範例的可以參照OpenAL: Managing Playback StreamsOpenAL short example。在OpenAL的開始需要ALCdevice與ALCcontext,範例如下
#include <AL/al.h>
#include <AL/alc.h>

int main()
{
//
  ALCdevice* pDevice=alcOpenDevice(NULL);
  if(pDevice==NULL)
    return 1;
  ALCcontext* pContext=alcCreateContext(pDevice,NULL);
  if(pContext==NULL)
  {
    alcCloseDevice(pDevice);
    return 1;
  }
  if(!alcMakeContextCurrent(pContext) )
  {
    alcDestroyContext(pContext);
    alcCloseDevice(pDevice);
    return 1;
  }
  //OpenAL was ready...
  //...
  //program end...
  alcDestroyContext(pContext);
  alcCloseDevice(pDevice);
  return 0;
}

alcMakeContextCurrent()非常重要,如果這個步驟沒成功,所有的OpenAL資源將無法製造。

  接著來製造播放聲音,範例如下
#include <AL/al.h>
#include <AL/alc.h>
#include <windows.h>
ALenum GetChannelFormat(short channels,short samples)
{
  ALenum val=AL_INVAILD_VALUE;
  switch(samples)
  {
  case 16:
    if(channels>1)
      val=AL_FORMAT_STEREO16;
    else
      val=AL_FORMAT_MONO16;
    break;
  case 8:
    if(channels>1)
      val=AL_FORMAT_STEREO8;
    else
      val=AL_FORMAT_MONO8;
    break;
  }
  return val;
}
int main()
{
//
  ALCdevice* pDevice=alcOpenDevice(NULL);
  if(pDevice==NULL)
    return 1;
  ALCcontext* pContext=alcCreateContext(pDevice,NULL);
  if(pContext==NULL)
  {
    alcCloseDevice(pDevice);
    return 1;
  }
  if(!alcMakeContextCurrent(pContext) )
  {
    alcDestroyContext(pContext);
    alcCloseDevice(pDevice);
    return 1;
  }
  //OpenAL was ready...
  ALuint sourceID;
  alGenSource(1,&sourceID);
  if(alGetError()!=AL_NO_ERROR)
  {
    alcDestroyContext(pContext);
    alcCloseDevice(pDevice);
    return 1;
  }
  //Follow data load from WAV file
  WAVEFORMATEX format;
  unsigned char* pWAVRawData;
  size_t wavRawDataSize;
  //
  ALunit bufID;
  alGenBuffers(1,&bufID);
  alBufferData(bufID,GetChannelFormat(format.nChannels,format.wBitsPerSample),pWAVRawData,format.nSamplesPerSec);

  //Enqueue buffer
  alSoruceQueueBuffers(sourceID,1,&bufID);
  //Play
  alSourcePlay(sourceID);
  //wait play end
  ALint sourceState;
  alGetSourcei(sourceID, AL_SOURCE_STATE, &sourceState);
  while(sourceState==AL_PLAYING)
    ::Sleep(1);//sleep 1ms
  //
  //program end...
  alcDestroyContext(pContext);
  alcCloseDevice(pDevice);
  return 0;
}

流程的部分跟XAudio2很像,製造播放聲音的元件再推入Buffer來播放聲音,利用alGetSourcei()來確認是否播放完畢。

  最後來說Stream play的部分,XAudio2的Dequeue是自動的,但OpenAL的Dequeue的流程要手動,範例如下
ALuint tempBufferID=0;
alSourceUnqueueBuffers(sourceID,1,tempBufferID);
if(tempBufferID!=0)
{
  //Dequeue ok!
  //...
}

過程不會太難,但什麼時候會Dequeue會失敗?當這個Buffer還在播放中的話,BufferID就會錶持為0,如果你看OpenAL: Managing Playback Streams裡的話,會發現它使用alGetSourcei()透過AL_BUFFERS_PROCESSED來得知Buffer是否播放完畢,但查到這篇AL_BUFFERS_PROCESSED bug?所以我不打算用範例的方法檢測是否播完。

  整體來說XAudio2與OpenAL的用法很像,目前差異只有在Dequeue的部分,如果要整合的話相信並不困難,OpenAL可以用在Windows與Linux,但XAudio2卻只能跑在Windows,整合到底有沒有價值?只能日後再來檢驗了。

參考資料

ALSA
OpenAL官網
OpenAL: Managing Playback Streams
OpenAL short example
AL_BUFFERS_PROCESSED bug?

相關文章

WAV檔案格式學習心得
初探XAudio2

2018年12月3日 星期一

初探XAudio2

初探XAudio2

前言

  最近要在遊戲引擎裡播放聲音,本想繼續使用以前的DirectSound來播,但查了一下DirectSound的wiki得知DirectSound已不建議使用,所以打算改成XAudio2來播放聲音,以下是學習心得,在此做個紀錄。

內容

  在使用XAudio2的開始需要一些範例,但不幸的是Windows10本身沒給範例,所以請到舊的DirectX範例裡下載,裡面有個"XAudio2"的資料夾,範例就在裡面。

  使用XAudio2時需要"IXAudio2",所有的資源都由這個COM來管理,有點類似OpenGL的context,範例如下
HRESULT hr = CoInitializeEx( nullptr, COINIT_MULTITHREADED );
if (FAILED(hr))
{
  wprintf(L"Failed to init COM: %#X\n", hr);
  return 0;
}
//
IXAudio2* pXAudio2 = nullptr;
hr = XAudio2Create( &pXAudio2 , 0);
if( FAILED( hr ) )
{
  wprintf( L"Failed to init XAudio2 engine: %#X\n", hr );
  CoUninitialize();
  return 0;
}

在CoInitializeEx()的部分並不是每次要XAudio2Create()之前都要喚起,而是整個程式只要在最開頭喚起一次就好,當然在程式最後也請喚起CoUninitialize()。XAudio2Create()可以依據需要填 入debug的flag,範例裡有示範。

  接著來製造"IXAudio2MasteringVoice",這個COM其實我並不是很了解它的作用,但一定要製造,製造的方法很簡單,範例如下
//IXAudio2* pXAudio2;
IXAudio2MasteringVoice* pMasteringVoice = nullptr;
if( FAILED( pXAudio2->CreateMasteringVoice( &pMasteringVoice ) ) )
{
  pXAudio2->Release();
  CoUninitialize();
  return 0;
}

  接著來製造"IXAudio2SourceVoice",這個就是在播放聲音的COM,依據需要"同時"播放幾個聲音來決定需要的數量,製造前需要決定播放的格式,這個結構是"WAVEFORMATEX",這個結構的資料可以在WAV檔裡的"fmt"的chunk裡找到,可以參考前篇WAV檔案格式學習心得,範例如下
//IXAudio2* pXAudio2;
IXAudio2MasteringVoice* pMasteringVoice;
WAVEFORMATEX format;
//Fill struct data
//...
IXAudio2SourceVoice* pSourceVoice = nullptr;
if( FAILED( pXaudio2->CreateSourceVoice( &pSourceVoice, &format) ) )
{
  return 0;
}

接著將WAV的raw data送到"IXAudio2SourceVoice",範例如下
//IXAudio2* pXAudio2;
IXAudio2MasteringVoice* pMasteringVoice;
IXAudio2SourceVoice* pSourceVoice;

unsigned char* pWAVRawData;
size_t wavRawDataSize;
XAUDIO2_BUFFER buffer = {0};
buffer.pAudioData = pWAVRawData;
buffer.Flags = XAUDIO2_END_OF_STREAM;
buffer.AudioBytes = wavRawDataSize;
if( FAILED(pSourceVoice->SubmitSourceBuffer( &buffer ) ) )
{
  pSourceVoice->DestroyVoice();
  return 0;
}
這裡要注意"IXAudio2SourceVoice"的釋放不是用Release(),而是用DestroyVoice(),接著只要使用Start()
來播放即可,要停止播放使用Stop()。

  最後來說說Stream play,請看下圖
Stream play
播放聲音時可以不斷地往後送Buffer,在XAudio2裡這個動作就是SubmitSourceBuffer(),也就是圖中的"Enqueue",圖中的"Dequeue"這個動作在XAudio2是"自動完成"的,可以利用"IXAudio2SourceVoice"下的GetState()來取得剩下的Buffer數量,這就是Stream play。這種機制幾乎是必要的,想想如果聲音檔案很長,用一般的做法(只播一個Buffer),這個Buffer就會很大,而且考量到壓縮格式的聲音檔(如Ogg),必須是解一小段播一小段(這樣很省記憶體),同樣的機制在DirectSound要實現的話相當麻煩,還必須處理Event,程式碼相當長,但XAudio2在這方面的實現就相當簡單,最後我想以後我應該會淘汰DirectSound改用XAudio2。

參考資料

DirectSound的wiki
舊的DirectX範例

相關文章

WAV檔案格式學習心得

2018年11月26日 星期一

WAV檔案格式學習心得

WAV檔案格式學習心得

前言

  最近接合Audio API到遊戲引擎上,但Audio API都不提供Parse檔案相關的功能,所以必須要自己Parse後萃取出raw data,再將raw data送入Audio API。以下是學習心得,在此做個紀錄。

內容

  WAV檔的格式是由很多的RIFF chunk所組成,檔案組成的概況如下圖
WAV檔的組成
每個RIFF chunk裡都包含了RIFF header與Content,每個RIFF header會用chunkID來表明資料的型態,而size所表明的是RIFF chunk裡的Content資料有多少Bytes。在Parse時,常常需要知道現在的RIFF chunk有多少Bytes,計算法如下
size_t riffChunkSize=riffHeader.size+sizeof(CRIFFHeader);

  接著來看看WAV檔裡的Chunk的Content,Content有多少種呢?不幸的很多種,所以這裡只列出我有遇到的種類。
chunkID是個32位元的資料,也就是4個Bytes,通常會將每個Byte表達成char,這樣就可以4個char來表達資料種類。

  第一種的chunkID為'R','I','F','F',這個很特別,它通常會是第一個RIFF chunk,特別的是它的content,一定是一個int32_t,且內容是'W','A','V','E',但是它的RIFF header裡的size的內容並不是4,而是整個WAV檔的大小減8,這個8其實就是RIFF header的大小,有點類似資料夾的概念,這個chunk包住了其它的chunk。

  第二種的chunkID為'f','m','t',' ',這裡的Content可以參考WAVEFORMATEX structure,但要注意struct裡的"cbSize",這個欄位不一定存在,所以這個chunk的size不是固定的!請依據chunk裡的size來Parsem。

  第三種的chunkID為'f','a','c','t',這個chunk的內容我並不知它的Content是什麼,我選擇直接跳過它!請注意這個chunk並不是一定存在,parse時請考慮這點。

  第四種的chunkID為'd','a','t','a',這裡就是raw data,也就是Audio API吃的資料。

  最後把前述的種類排列起來就會像下圖
Wav檔的Chunk排列

參考資料

WAVE PCM soundfile format
WAVEFORMATEX structure

2018年11月19日 星期一

ndk-build的使用心得

ndk-build的使用心得

前言

  最近將繪圖引擎跨到Android,在繪圖API的部分會面臨到選擇GLES的版本的問題,還有像是GLES3只有64位元支援,如果只靠修改Android.mk與Application.mk的話會沒辦法做出像是組態(Configuration)的效果,所以對ndk-build做了研究,在此做個紀錄。

內容

  ndk-build本身不提供組態(Configuration)的功能,但可以透過參數指定Android.mk與Application.mk,這做法可以做出組態(Configuration)的效果,但只要想想組態越多,Android.mk與Application.mk的數量也會隨著組態而增多,GLES的版本目前有GLES2、GLES3、GLES3.1與GLES3.2,如果用這個方法就會有8個檔案要維護,加上共用的部分還要隔開的成本,做了一下我就放棄這個做法,因為成本有點高。

  在google一陣子後發現ndk-build可以直接從命令列新增或修改在Android.mk與Application.mk的參數,比方說"APP_ABI"這個參數,在入門教學中會把該參數寫在Application.mk,用來決定要建置的ABI,但事實上是可以用ndk-build的命令列來修改,命令如下
ndk-build APP_ABI=arm64-v8a

當用這個命令後,請移除Application.mk裡的"APP_ABI"的設定,不然ndk-build社的數值會被覆蓋,如果要設定多個參數就用空白隔開即可。只靠這個方法來實現組態會不太夠,必須再搭配makefile的if語法,語法的範例如下
ifeq ($(GLES_VERSION),GLES2)
GLES_LIBS := -lGLESv2
endif
ifeq ($(GLES_VERSION),GLES3)
GLES_LIBS := -lGLESv3
endif
ifeq ($(GLES_VERSION),GLES3_1)
GLES_LIBS := -lGLESv3
endif
ifeq ($(GLES_VERSION),GLES3_2)
GLES_LIBS := -lGLESv3
endif

範例中使用"GLES_VERSION"這個變數來決定最後的要載入的LIB,"GLES_VERSION"是自訂的,搭配之前的方法可以在ndk-build的命令列隨喜好來修改,這樣不論是選GLES版本或者是只能建64位元版本都可以解決了!

  ndk-build在指定ABI的版本來建置後,會把非建置版本的ABI移除,例如APP_ABI=arm64-v8a時,x86_64、armeabi-v7a與x86都會被移除,這個問題目前解決的方案不是很好,就是每個ABI都分不同資料夾來建置,可以在ndk-build的命令列修改"NDK_OUT"與"NDK_LIBS_OUT"來完成,這個方法可行,但就是覺得不太優雅,有機會再找找有沒有優雅的解決方案。

  ndk-build的clean有點討厭,它只為清除LIB,但不會清除OBJ,如果需要完整重建的話,就手動清空或是寫個命令來清除。

  最後來說一下命令的部分,我個人是使用Python來執行ndk-build並選擇組態(Configuration),不選擇Shell與bash只是單純地不熟,但Python雖然跨平台,但有新舊版本的問題(Python2.7與Python3),這部分的實現目前就這樣解決,如果有更好的方法以後再來說明。

參考資料

Application.mk
ndk-build

2018年11月12日 星期一

在Android裡開啟Dialog

在Android裡開啟Dialog

前言

  在之前的這篇"在Android實現多個按鍵同時按壓"有提及要做一個輸入Console命令的視窗,該篇說到了開啟子視窗的方法,這篇來實作UI的部分,在此做個紀錄。

內容

  Android並沒有視窗UI,而"Dialog"的功能可以做出像在Windows開子視窗,所以本篇會使用Dialog來實現,在找資料的過程中,還找到了也可以做出類似效果的"AlertDialog",所以方法並不是只有一種,有機會再來說明"AlertDialog"。

  開啟一個"Empty activity"的專案,接著要新增一個"Layout",具體的操作如下圖
在layout資料夾新增xml
在layout資料夾點擊右鍵後,依據上圖的操作新增xml,接著會看到下圖
新增layout的設定視窗
看到新增layout的設定視窗後,更改layout的名稱,本篇使用"sub_dialog",可以自己命名,但要注意不能有大寫的文字。新增完後開啟該xml檔,依據下圖操作
修改layout的內容
依據上圖開啟layout的文字模式,輸入以下xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:ems="10"
        android:inputType="textPersonName"
        android:text="Name" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Close" />

</LinearLayout>


這個xml會規劃一個輸入的text與一個關閉的按鈕。

  接著在activity的layout新增一個按鈕,用來開啟Dialog,操作如下圖
新增開啟Dialog的按鈕
按鈕增加完後,接著在Activity裡開啟,範例碼如下
package com.hosee.hellodialog;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.app.Dialog;
import android.util.Log;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity {
    Dialog subDialog;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btnOpenDialog = (Button)findViewById( R.id.button2);
        btnOpenDialog.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                subDialog=new Dialog(MainActivity.this);
                subDialog.setTitle("Title");
                subDialog.setCancelable(false);
                subDialog.setContentView(R.layout.sub_dialog);

                Button btnClose=(Button)subDialog.findViewById(R.id.button);
                btnClose.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        EditText editText=(EditText)subDialog.findViewById(R.id.editText);
                        Log.i("HelloDialog","Input:"+editText.getText() );
                        subDialog.cancel();
                    }
                });
                subDialog.show();
            }
        });
    }
}


範例碼有關於View的ID可能會與範例碼的ID不同請依據layout的命名來修改。在"btnOpenDialog"就是Activity用來開啟Dialog的按鈕,為它設定"Click"事件,在"Click"事件中,會看到開啟Dialog的過程,setTitle()可以設定Title的顯示名稱,似乎不能省略,setCancelable()設為false表示當使用者點擊Dialog是窗外的範圍時"不會"關閉Dialog,接著會看使用"subDialog"來執行findViewById(),而不是直接用activity的findViewById(),這裡要注意!然後設定關閉按鈕的Click"事件,這裡會再關閉前列印輸入text的內容後執行"subDialog.cancel()",這道命令會真正的關閉Dialog,在將事件都設定完後可以看到"subDialog.show()",這道命令會開啟Dialog,完成的結果如下圖
執行結果


參考資料


相關文章

在Android實現多個按鍵同時按壓

2018年11月5日 星期一

在Android實現多個按鍵同時按壓

在Android實現多個按鍵同時按壓

前言

  由於繪圖引擎跨到Android時是全螢幕狀態,加上考慮接合Android的事件如Click、Touch...等,這時多了個麻煩,要如何輸入Debug的命令?在PC不論是Window或Linux都有Console視窗,但Android卻沒有,所以目前想到的是在螢幕的左上與右下新增隱形的按鈕,當同時壓下時會出現UI可以輸入命令。這次只記錄同時按壓的部分,在此做個紀錄。

內容

  先在Android studio裡開一個"Empty activity"的專案,開完後到Layout裡新增2個TextView,如下圖
在Layout新增按鍵
按鍵是用TextView,而不是Button,因為TextView當裡面的text是空值時就可以變成隱形的按鈕,範例為了演示而未清空。要注意圖中右邊的ID與對其都要調整,ID的部分為textView1與textView2,而對齊分別為左上與右下。

  調整為Layout後,接著就來偵測同按壓時,範例如下
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.util.Log;
import java.util.Date;
public class MainActivity extends AppCompatActivity {
    private long time1=0;
    private long time2=0;
    private void CheckMultiClick(){
        long delTime=time1-time2;
        if(delTime < 0)
            delTime*=-1;
        //
        if(delTime<1000){
            Log.i("MultiClick","Double click!");
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv1=(TextView)findViewById(R.id.textView1);
        TextView tv2=(TextView)findViewById(R.id.textView2);
        //
        tv1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                time1=(new Date() ).getTime();
                CheckMultiClick();
            }
        });
        //
        tv2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                time2=(new Date() ).getTime();
                CheckMultiClick();
            }
        });
    }
}

範例中,先分別為TextView上OnClick事件,可以看到當Clcik發生時會記錄"time",而這個"time"的意義可以參考Java日期時間(Date/Time),紀錄完後喚起"CheckMultiClick()",裡面很簡單,將兩個"time"的差做檢查是否小於1000,這裡的時間差代表兩個"time"差幾毫秒(milliSecs),所以1000代表差1秒就發生同時按壓。

參考資料

Java日期時間(Date/Time)

2018年10月29日 星期一

從OpenGL跨到OpenGLES的心得

從OpenGL跨到OpenGLES的心得

前言

  最近將C++的繪圖引擎跨到Android,PC端使用的是OpenGL4.6,Android端使用的是OpenGLES3.2,目前只完成初步的測試,就是只能用基本的Shader畫三角形。這次我做了個挑戰,由於手機有支援OpenGLES3.2,所以直接跨到OpenGLES3.2,果不其然碰到了不少問題,這裡做個紀錄。

內容

  從OpenGL跨到OpenGLES的差異這裡分成"Context的差異"與"API的差異"兩部分。Context的差異是必然的,因為PC端的OpenGL為Windows與Linux分別準備了WGL與GLX,自然也別期望android平台可以用一樣的方法,而API的差異部分比想像來的少,透過docs.gl可以查詢OpenGL與OpenGLES的差異。

  Context的差異的部分主要有兩大問題,"執行序(Thread)的問題"與"視窗可能被銷毀的問題",執行序(Thread)的問題主要是因為OpenGL與OpenGLES的Context都會認執行序,也就是用哪個執行序創建Context就只能用該執行序來執行繪圖,這個規則在PC時問題不大,但在Android時會有麻煩,由於Android的Activity不提供類似main()的Function,所以要自己做一個執行序(Thread)來當作main(),這時候就可能發生創建Context與繪圖的執行序(Thread)不是同一個,而且不幸的是這件事情發生時不會有任何的報錯!只會得到一個黑畫面而已,一定要確保創建Context、執行繪圖與銷毀Context都是用C++創建的執行序(Thread),接合Android的Activity的事件只能用來通知有事件發生,不能執行任何EGL與OpenGLES的API。視窗可能被銷毀的問題是Android跟Windows與Linux很大的不同,在Windows與Linux把視窗縮到最小是將視窗Hide的意思,但Android很不一樣,當進入休眠或跳出程式時,都會去銷毀視窗!還記得EGL創建Context時用的eglMakeCurrent()嗎?裡面的Surface參數會在進入休眠或跳出程式時失效,所以必須要對該事件做處理,處理的流程可以參考Android 前后台切换与OpenGL(EGL)创建销毁的周期,裡面有個ResetSurface()可以在事件發生時喚起。

  API的差異部分如下:
1.不支援glDepth(),請用glDepthf()來替代
2.不支援GL_TEXTURE_1D,如果需要1D的Texture請用2D替代
3.不支援glMapBuffer(),寫UBO的話請用glBufferSubData()替代
4.GLSL編譯的時候請將"#version 300 es"放在最開頭
5.Extension不互通,如在OpenGL判斷是否支援UBO可以透過Extension檢查,OpenGLES則是版本來決定(OpenGLES3以後就支援)

前4點都還好很容易克服,第五點就比較頭痛了,必須知道各個版本的規格,只能用經驗來克服了。

  最後再說個關於include的問題,如果查詢Android NDK Native APIs時會發現以下畫面
OpenGLES3.2的include
這個include是錯的!如果去NDK把"GLES3/gl3ext.h"打開的話,就會發現這個Header是個只有註解的空檔案,正確的include如下
#include "GLES3/gl32.h"
#include "GLES2/gl2.ext.h"

雖然看起來有些奇怪,但如果不這麼做的話,會沒辦法創建壓縮格式的Texture,如ASTC、PVR...等,因為Marco都定義在"GLES2/gl2ext.h",由於OpenGLES目前只有跨到Andoird,不知這是不是OpenGLES的標準用法,只能日後再確認了。

參考資料

Android NDK Native APIs
docs.gl
Android 前后台切换与OpenGL(EGL)创建销毁的周期

2018年10月22日 星期一

C++的IO在跨平台時的問題

C++的IO在跨平台時的問題

前言

  最近將C++的繪圖引擎從Windows轉到Linux,本想問題比較大的應該是OpenGL的部分,但結果是我花在OpenGL的部分的時間沒想像的多,反倒是其它的問題讓我花了很多時間Debug,而這個問題跟C++的IO有關,以下是Debug的過程,在此做個紀錄。

內容

  在繪圖引擎轉到Linux後,花了些時間解決OpenGL在跨平台的問題,接著本想就沒什麼大問題了,但實際上卻是Shader的部分卻在Run的時候初始化失敗,本想是程式的工作目錄的問題,但結果並不是,Debug後發現檔案是有讀到的,但Shader的初始化這段程式碼在Windows跑的時候都沒問題,難道Shader的初始化有分平台!?

  在此說明一下,在我的繪圖引擎的Shader初始化是將檔案讀入後,在經由Parser取得Shader的資訊後,再將shader code送進OpenGL的API。Debug後發現問題發生在Parser取得Shader的資訊的過程,由於這個Parser做得有點複雜,所有我花了大量的時間在Debug這個Parser,在Debug時候一直有個想法,不就是個Parser,為什麼Windows與Linux的結果會不一樣?最後我找到兇手了,兇手來自以下的程式碼
std::string shaderCode;
std::ifstream fileStream(shaderFilePath);
if(!fileStream)
  return false;
shaderCode.assign((std::istreambuf_iterator<char>(fileStream) ),std::istreambuf_iterator<char>() );
//start parse
//...


return true;

這段程式碼將檔案讀入後送到Parser,但我發現"shaderCode"的內容在Windows與Linux的結果是不一樣的!由於Shader檔案是在Windows的文字編輯器打的,所以換行的字元是"\r\n",這段程式碼在VC(visual c++)跑的時候會將'\r'過濾掉,但是在g++的時候卻不會,所以造成之後的Parser會Parse出不同的結果,造成初始化錯誤。

  所以要如何解決這個問題呢?解決的方法並不是改變這段讀取檔案的程式碼,因為如果這樣解決的話代表Parser本身不容許'\r'在字尾,所以比較好的解法是讓Parser不要單單使用Split的方式來取得,而是在每次Split後也同時做Trim的動作,這樣就可以讓Parser接受有\r'的內容了。

參考資料

How to read line by line or a whole text file at once?

2018年10月15日 星期一

在Android studio裡設定NDK

在Android studio裡設定NDK

前言

  最近打算讓C++的引擎支援Android,所以就安裝許久沒用的Android studio,裝完後發現NDK的用法跟我以前的用法差很多,所以想說可以換回以前的"ndk-build.cmd"嗎?答案是可以的,設定的過程不太好記,所以在此做個紀錄。

內容

  在設定NDK的第一步驟是安裝NDK,Android studio本來就有整合,只是預設不會裝。首先打開SDK Manager,如下圖
打開SDK Manager

接著選取安裝NDK,如下圖
安裝NDK

安裝完後請重新開啟Android studio。

  下一個步驟是設定External Tools,這裡會新增兩個工具,一個是javah,另一個是ndk-build。
打開設定,如下圖
打開設定

接著到External Tools新增工具,如下圖
External Tools新增工具

新增"javah",如下圖
新增"javah"
Program的參數
$JDKPath$\bin\javah.exe

Arguments的參數
-classpath "$Classpath$;" -d $ModuleFileDir$\src\main\jni $FileClass$

Working Directory的參數
$ModuleFileDir$\src\main\java\
接著新增ndk-build",如下圖
新增"ndk-build"

Program的參數
C:\Users\hosee\AppData\Local\Android\Sdk\ndk-bundle\ndk-build.cmd

Working Directory的參數
$ModuleFileDir$\src\main\
Program的參數的參數因為沒marcro,所以是個絕對位置,其實就是NDK的安裝位置,如果不要用Android studio裡的NDK,
可以把這裡的位置改掉。
接著設定打開專案結構,如下
打開專案結構

接著設定JDK,使用Android studio裡面的JDK,如下圖
設定JDK
設定的部分到此結束,接著是在專案中使用NDK。

  開一個新專案後,在gradle.properties新增以下
android.useDeprecatedNdk=true

如圖
修改gradle.properties

然後在build.gradle新增以下
ndk {
  moduleName "HelloNDK"
  ldLibs "android"
}
sourceSets.main {
  jni.srcDirs = []
  jniLibs.srcDir "src/main/libs"
}

如圖
修改build.gradle

修改完後,再來是新增jni的資料夾,如下圖
新增jni資料夾

新增後會跳出"New Android Component",直接Finish即可,如下圖
"New Android Component"視窗

  在專案的Activity裡新增以下
    static {
        System.loadLibrary("HelloNDK");
    }

    public static native String nativeHello();

如圖
新增NDK介面

介面加完後,使用"javah"來新增header檔。

  接著在jni資料夾新增source檔,檔案內容如下
#include "string.h"
#include "jni.h"
#include "com_example_hosee_hellondk_MainActivity.h"
JNIEXPORT jstring JNICALL Java_com_example_hosee_hellondk_MainActivity_nativeHello(JNI* pEnv,jclass jObj)
{
  return (pEnv)->NewStringUTF("Hello NDK!");
}

操作如圖
新增source檔

  接著在jni資料夾新增"Android.mk",內容如下
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := HelloNDK
LOCAL_SRC_FILES := com_example_hosee_hellondk_MainActivity.cpp
LOCAL_LDLIBS += -landroid
include $(BUILD_SHARED_LIBRARY)

操作如圖
新增"Android.mk"

  接著在jni資料夾新增"Application.mk",內容如下
APP_PLATFORM := android-15
APP_MODULES := HelloNDK
APP_ABI := all

操作如圖
新增"Application.mk"

  接著使用ndk-build產生so檔,如下圖
使用ndk-build產生so檔

最後在專案的Activity新增以下
TextView tv = (TextView) findViewById(R.id.TestTextView);
tv.setText(nativeHello() );

操作如圖
使用NDK的Function

這裡要注意TextView的ID要先去Layout裡面加完,加完後就可以使用了。


參考資料

超级简单的Android Studio jni 实现(无需命令行)

2018年10月8日 星期一

在Ubuntu裡切換GPU

在Ubuntu裡切換GPU

前言

  在筆電安裝Ubuntu後,發現Chrome跑個網頁相當吃力,本想可能是選到Intel的GPU,想做個交換GPU,發現根本沒選項,所以就來研究怎麼切換GPU,在此做個紀錄。

內容

  研究後發現Ubuntu根本沒安裝顯卡驅動,所以跑個網頁就相當吃力。怎麼安裝顯卡驅動呢?
在"系統設定值"裡有個"軟體與更新",如下圖
軟體與更新
進去後選取"額外的驅動程式"頁籤,如下圖
安裝驅動程式
可以看到下方有個列表,預設的驅動程式是在第三個選項,將它選到第一個選項進行安裝,安裝後再重新啟動。

  安裝完驅動程式後,可以開啟顯卡驅動所附帶的軟體來切換GPU,以nVidia的為例就是開啟"NVIDIA X Server Settings",如下圖
NVIDIA X Server Settings

開啟後,在左側選取選項"PRIME Profiles",如下圖
選取GPU
接著就可以在右側進行切換GPU了。

參考資料

Switch Between Intel and Nvidia Graphics Card in Ubuntu 16.04

2018年10月1日 星期一

使用python來備份SVN

使用python來備份SVN

前言

  備份SVN這件事以前都不是我會負責的事,但一定有做,最近添購了一台NAS,想想是個機會,就來研究一下,在此做個紀錄。

內容

  如果需要備份SVN的話就需要"svnadmin",所以請先確認該命令是否可以使用。備份SVN的命令如下
svnadmin dump c:\svnrepo --incremental > c:\backup\xxx.dump

紅色的部分是檔案庫的位置,藍色的則是輸出的備份檔,請依據需求更改。如果要用備份檔來還原檔案庫的話命令如下
svnadmin load c:\svnrepo < c:\backup\xxx.dump

紅色的部分是還原檔案庫的位置,藍色的則是使用的備份檔。整個過程用起來相當簡單,但備份這件事通常會希望是"自動執行"的,而不是需要用"手動執行"命令才動作,所以接著來介紹如何自動執行。

  排程這個功能在Windows與Linux都有這個功能,它可以用來每隔一段時間來時執行某個命令,幸運的,買來的NAS有視窗化的操作來編輯排程,所以這部份很快就完成了,至於Windows與Linux的排程實際上怎麼使用以後有機會再研究。

  使用Python來執行備份命令,為什麼不直接用bash呢?唯一的考量就是跨平台,加上Python使用起來比較靈活,所以就決定用Python了。執行的時候通常會希望可以依據備份的時間來分資料夾,所以我實作了以下的範例碼
import os
import time
backUpRootDir = '/homes/hosee/SVNBackUp/'
backUpFileName = 'svnBackUp.dump'
#
dirName = time.strftime("%Y%m%d_%H%M%S", time.localtime() )
os.system('mkdir -p '+backUpRootDir+dirName)

os.system('svnadmin dump /homes/hosee/svn/repo --incremental > '+backUpRootDir+dirName+'/'+backUpFileName)

這個範例碼會依據備份的時間製作資料夾,並在該資料夾儲存備份檔。範例碼的路徑請依據需要修改,如"backUpRootDir"與svnadmin的檔案庫位置。最後,就是只要用排程來啟動Python的script就完成了。

參考資料

subversion的備份hotcopy以及dump的使用

2018年9月24日 星期一

初探Chromium Embedded Framework

初探Chromium Embedded Framework

前言

  Chromium Embedded Framework以下簡稱為"cef",是一個可以在Native App裡使用Chrome的Framework,知名的"Electron"的前端就是"cef",由於是個開源專案,所以來研究一下在此做個紀錄。

內容

  如果上網搜尋很容易會找到開源專案的位置 Chromium Embedded Framework Overview,開啟後會得到Git的URL,如下圖
cef的Git
如果就這麼開開心心的用Git直接Clone的話,就等於在"浪費時間",因為那個Git並不是用來給使用者存取的,而是給"cef"的自動建置流程來存取的!所以下載完後會發現根本不能建置。那麼該如何建置"cef",請參考MasterBuildQuickStart

  在開啟MasterBuildQuickStart後,先看"File Structure",如下圖
cef的File structure
這個"File Structure"有一部分是要手動建構的,不是自動產生的,該網頁還會說明各個平台建置需求與步驟,以下說明為Windows平台。雖說網頁上說要準備40G的空間,但我實際建置64位元Debug版後,發現大概要55G,建立前請確認空間是否充足。

  第一步是建立"automate"與"chromimu_git"資料夾,說明的Root資料夾為"c:\code",請示需求更改Root資料夾。

  第二步是在Root資料夾下建立"depot_tools"資料夾,並下載說明中的"depot_tools.zip",如下圖
下載"depot_tools.zip"
載完後,解壓到"depot_tools"資料夾下即可。

  第三步是執行"update_depot_tools.bat",這個檔案在"depot_tools"資料夾裡,但在執行前請確認Python的版本,限定2.7.x版才可以成功建置,如果是3以後的版本請切換。

  第四步是將"depot_tools"資料夾的位址加到環境變數"path"裡。

  第五步是下載"automate-git.py",如下圖
下載"automate-git.py"
下載後放到"automate"資料夾下。

  第6步是製造"update.bat",到"chromimu_git"的資料夾下新增一個"update.bat",內容如下
set GN_DEFINES=use_jumbo_build=true
set GN_ARGUMENTS=--ide=vs2017 --sln=cef --filters=//cef/*
python ..\automate\automate-git.py --download-dir=c:\code\chromium_git --depot-tools-dir=c:\code\depot_tools --no-distrib --no-build

注意一下紅色的內容,有兩個檔案位置會因為Root資料夾的位置不一樣而改變,請示需要修改。改完後直接執行"update.bat"。這個步驟有點就大概要一個半小時,執行中如果中斷就要重來,請先注意。

  第7步是製造"create.bat",到"(root)\chromimu_git\chromium\src\cef"下,製造"create.bat",內容如下
set GN_DEFINES=use_jumbo_build=true
set GN_ARGUMENTS=--ide=vs2017 --sln=cef --filters=//cef/*
call cef_create_projects.bat

改完後就直接執行。

  第8步就是啟動自動建置,到"(root)\chromimu_git\chromium\src",執行以下命令
ninja -C out\Debug_GN_x64 cef

這種個建置過程非常久,在我的電腦是3個小時,請注意。如果建置完成就可以在"(root)\chromimu_git\chromium\src\out\Debug_GN_x64"下找到"cefclient.exe",執行後就可得到一個cef的視窗。


參考資料

MasterBuildQuickStart

2018年9月17日 星期一

DirectInput的學習心得(2)

DirectInput的學習心得(2)

前言

  續前篇DirectInput的學習心得(1),本次紀錄用DirectInput來偵測手把(Gamepad)。為何不直接使用XInuput呢?為了相容一些較舊的手把,就這麼簡單。如果同時使用DirectInput與XInuput時會有一些要注意的,就是DirectInput可以抓到XInuput的手把,本篇會記錄如何濾除,在此做個紀錄。

內容

  在開始偵測手把時,要先製造出IDirectInput8,如果不知道如何製造,請參照前篇DirectInput的學習心得(1),接著跟上次鍵盤的狀況不太一樣,由於系統鍵盤只會有一個,所以不需要去列舉裝置,但手把可能不只一個,所以透過EnumDevices()來抓出有多少個手把,接著對每個手把做SetCooperativeLevel()與SetDataFormat(),這部分和上次的鍵盤一樣,但這次要多一個步驟,就是透過EnumObjects()來取得類比按鈕的資訊,最後完成後一樣要Acqurie()。
  來看看範例程式,如下
#include "dinput.h"
#include "dinputd.h"
struct SEnumJoystickContext
{
  IDirectInput8* pDirectInput;
  HWND hWnd;
  std::vector<IDirectInputDevice8>& deviceList;
};
BOOL CALLBACK EnumJoystickCallBack(const DIDEVICEINSTANCE* pdidInstance,VOID* pContext)
{
  //Cast context...
  SEnumJoystickContext* pEnumJoystickContext=(SEnumJoystickContext*)pContext;

  //
  LPDIRECTINPUTDEVICE8 pNewDevice=NULL;
  HRESULT hr=pEnumJoystickContext->pDirectInput->CreateDevice(pdidInstance->guidProduct,&pNewDevice,NULL);
  if(hr != DI_OK)
    return DIENUM_STOP;
  //
  pEnumJoystickContext->deviceList.push_back(pNewDevice);
  return DIENUM_CONTINUE;
}
BOOL CALLBACK EnumObjectsCallBack(const DIDEVICEOBJECTINSTANCE* pdidoi,VOID* pContext)
{
  IDirectInput8* pDevice=(IDirectInput8*)pContext;
  if(pdidoi->dwType &DIDFT_AXIS)
  {
    DIPROPRANGE diprg;
    diprg.diph.dwSize=sizeof(DIPROPRANGE);
    diprg.diph.dwHeaderSize=sizeof(DIPROPHEADER);
    diprg.diph.dwHow=DIPH_BYID;
    diprg.diph.dwObj=pdidoi->dwType;
    diprg.lMin=-1000;
    diprg.lMax=1000;
    //
    if(pDevice->SetProperty(DIPROP_RANGE,&diprg.diph)!=DI_OK)
      return DIENUM_STOP;
  }
  return DIENUM_CONTINUE;
}
void DetectedGamepad(IDirectInputDevice8* pDIDeviceGamepad)
{
  DIJOYSTATE2 gamepadState;
  pDIDeviceGamepad->GetDeviceState(sizeof(DIJOYSTATE2), &gamepadState);
  //Get x axis was left
  if(gamepadState.lX < 0)
  {
    //do something...
  }
  //Get button 0 state
  if(gamepadState.rgbButtons[0] &0x80)
  {
   //do something...
  }
}
int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow )
{
  //Init main window and IDirectInput8
  //...
  HWND mainWin = initMainWindow;
  IDirectInput8* pDirectInput = initDirectInput;
  std::vector<IDirectinputdevice8*> deviceList;
  //
  SEnumJoystickContext enumJoystickContext = { pDirectInput, mainWin, deviceList};
  pDirectInput-&gt;EnumDevices(DI8DEVCLASS_GAMECTRL, EnumJoystickCallBack, &enumJoystickContext, DIEDFL_ATTACHEDONLY) 
  for(int i=0;i<deviceList.size();i++)
    pGamepadDevice->SetCooperativeLevel(mainWin,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
    pGamepadDevice->SetDataFormat(&c_dfDIJoystick2);
    //
    pGamepadDevice->EnumObjects(EnumObjectsCallBack, pGamepadDevice, DIDFT_ALL);
    pGamepadDevice->Acquire();
  }
  //Init gamepad end...
}

在製造完IDirectInput8後,會看到一個SEnumJoystickContext的Struct與 EnumDevices(),SEnumJoystickContext是要傳給EnumDevices()的參數,再看看EnumDevices()的參數會發現一個CallBack function名為EnumJoystickCallBack,這個CallBack 會在每次被IDirectInput8列許到裝置後被喚起,看看喚起的EnumJoystickCallBack(),有個引數名為"pContext",會發現在開頭就被轉型為SEnumJoystickContext,也會看到另一個引數"pdidInstance",這次必須透過DIDEVICEINSTANCE來製造出IDirectInputDevice8,這個CallBack 的回傳值有"DIENUM_CONTINUE"與"DIENUM_STOP","DIENUM_CONTINUE"表示持持續列舉,"DIENUM_STOP"則代表中止列舉,在EnumDevices()結束後,所有的IDirectInputDevice8都會在"deviceList",接著就初始化每個裝置。初始化裝置和鍵盤很像,一樣是SetCooperativeLevel()與SetDataFormat(),但這次要多一個步驟,就是EnumObjects(),EnumObjects()一樣需要一個CallBack ,這個CallBack 主要用來初始化類比按鍵,最後一樣要Acquire()。偵測的按鍵的部分可以參考範例的DetectedGamepad(),透過GetDeviceState()可以得到"DIJOYSTATE2",裡面就會放置所有按鍵的值,詳細地的說明可以到DIJOYSTATE2 Structure查詢。
  如果需要與XInuput同時使用,官方的提供的做法是過濾掉XInuput的裝置,官方提供一個檢查是否為XInuput裝置的function,這個function可以用在EnumJoystickCallBack()裡去做濾除的動作,詳細的程式碼可以在XInput and DirectInput裡找到。

參考資料

XInput and DirectInput
DIJOYSTATE2 Structure

相關文章

DirectInput的學習心得(1)

2018年9月10日 星期一

DirectInput的學習心得(1)

DirectInput的使用(1)

前言

  在偵測輸入時可以採用XInput來偵測,可以參考XInput的使用這一篇,但XInput並不支援鍵盤與滑鼠,且手把要是年代較久遠的話也會無法被XInput偵測到。為什麼不用訊息幫浦來偵測輸入呢?理由很簡單,作業系統的訊息幫浦沒辦法調整偵測的速度,像現代電玩時常需求的FPS60偵測速度是達不到的,就我以前實測的經驗,訊息幫浦不但偵測速度不夠,而且偶爾還有掉輸入(明明有輸入卻沒反應),雖然不知是否為偵測速度不夠造成,但用DirectInput偵測鍵盤的輸入,這個狀況就可以得到解決。這次的紀錄只會記錄到鍵盤的偵測與使用,在此做個紀錄。

內容

  使用DirectInput的過程並不會太繁雜,首先使用DirectInputCreate()製造出IDirectInput8,這個介面只需造一次即可,接著用IDirectInput8來製造出IDirectInputDevice8,而依據製造的參數不同可以讓IDirectInputDevice8代表鍵盤、滑鼠與手把,本篇將說明如何將IDirectInputDevice8初始化為鍵盤。
  初始化鍵盤的範例碼如下

#include <dinput>
#include <dinputd>
void DetectedKeyboardKeys(IDirectInputDevice8* pDIDeviceKeyboard)
{
  unsigned char keyState[256];
  if(pDIDeviceKeyboard->etDeviceState(256,keyState) != DI_OK)
    return;
  //if GetDeviceState() is fined,keyState will save all key state from keyboard...
  //Follow detect sample...
  //Is num 0 pressed? 
  if(keyState[DIK_0])
  {
    //do something...
  }
  //Is key a pressed? 
  if(keyState[DIK_A])
  {
    //do something...
  }
  
}
int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow )
{
  //Init main window
  //...
  HWND mainWin = initMainWindow;
  //Init DiectInput...
  IDirectInput8* pDirectInput = nullptr;
  if(DirectInput8Create(hInstance,DIRECTINPUT_VERSION,IID_IDirectInput8,(void**)&pDirectInput ,NULL) != DI_OK)
    return 1;
  //Init keyboard
  IDirectInputDevice8* pDIDeviceKeyboard = nullptr;
  if(pDirectInput->CreateDevice(GUID_SysKeyboard,&pDIDeviceKeyboard ,NULL) != DI_OK)
  {
    pDirectInput->Release(); 
    return 1;
  }
  //
  if(pDIDeviceKeyboard->SetCooperativeLevel(mainWin,DISCL_FOREGROUND|DISCL_NONEXCLUSIVE) != DI_OK)
  {
    pDIDeviceKeyboard->Release();
    pDirectInput->Release(); 
    return 1;
  }
  if(pDIDeviceKeyboard->SetDataFormat((DIDATAFORMAT*)&amp;c_dfDIKeyboard) != DI_OK)
  {
    pDIDeviceKeyboard->Release();
    pDirectInput->Release(); 
    return 1;
  }
  //
  pDIDeviceKeyboard->Acquire();
  //Init keyboard end
  // 
  return 0;
}

在主程式一如之前所說製造出IDirectInput8,要注意HINSTANCE是必要的參數,再來是透過 IDirectInput8的CreateDevice()來製造出IDirectInputDevice8,並可載參數上看到"GUID_SysKeyboard",這個參數會決定裝置是鍵盤、滑鼠或手把,接著是SetCooperativeLevel(),主要的目的確定要偵測哪個視窗的輸入,SetDataFormat()決定偵測資料的輸出格式,在鍵盤時這是固定的,所以用c_dfDIKeyboard來設定即可,接著Acquire(),這個過程很重要,初始化的最後一個流程,勢必要流程請勿省略,Acquire()如果失敗表示該裝置目前沒連上,以上就是初始化鍵盤的部分。偵測的部分在DetectedKeyboardKeys(),首先準備好一塊Buffer,範例使用的是keyState,256是鍵盤上所有的鍵的大小,透過GetDeviceState()來取得所有按鍵的輸入狀態,取完之後如何取得要偵測的按鍵的狀態呢?可以參考
Keyboard Device Enumeration,用該列舉來做為索引值取得目前按鍵的狀態,如果不是0就代表按下。

參考資料

Capturing DirectX DirectInput Keyboard and Mouse Input
DirectInput
Keyboard Device Enumeration

相關文章

DirectInput的學習心得(2)

2018年9月3日 星期一

從Blender裡萃取模組資料(8)

從Blender裡萃取模組資料(8)

前言

  續前篇從Blender裡萃取模組資料(7),這次說明萃取Skeleton的動畫,在此做個紀錄。

內容

  如果使用前篇的範例來萃取Skeleton的動畫,應該會發現竟然完全沒問題,可以正常執行,但若仔細一看"Data path"的欄位,就會發現它有何不同了,就拿"location"來說,在Skeleton的動畫可能會是"pose.bones["spine"].location",其中的"spine"為該Bone的名稱。這裡有個陷阱!如果你直覺地用字串的split()來parse這個"Data path"的欄位的話會有問題,問題會發生在Bone的名稱是可以包含".",舉例來說如果Bone的名稱是"spine.001"的話,"Data path"就會變成"pose.bones["spine.001"].location",接著split()後就變成"[pose, bones["spine, 001, location]",明顯的,Bone的名稱會被切到,還必須注意Blender的名稱可以很自由,名稱裡可以出現單引號或雙引號,所以需要特製的Parse。

  接著用前篇的範例為基礎,製作出以下範例
import bpy

def ShowActionInfo(act):
  print('animation name',act.name)
  for curve in act.fcurves:
    print('Data path:',curve.data_path,' ',end='')
    print('')
    if curve.data_path == 'location':
        print("Data from transform.location")
    elif curve.data_path == 'rotation_euler':
        print("Data from transform.rotation_euler")
    elif curve.data_path == 'rotation_euler':
        print("Data from transform.rotation_quaternion")
    elif curve.data_path == 'scale':
        print("Data from transform.scale")
    elif curve.data_path[:12] == 'pose.bones["':
        namelastIndex = curve.data_path.rfind('"')
        if namelastIndex >= 0:
            boneName = curve.data_path[12:namelastIndex]
            propertyNameIndex = curve.data_path.rfind('.')
            if propertyNameIndex >= 0:
                propertyName = curve.data_path[ (propertyNameIndex+1):]
                if propertyName == 'location':
                    print('Bone:',boneName,' Property:transform.location')
                elif propertyName == 'rotation_euler':
                    print('Bone:',boneName,' Property:transform.rotation_euler')
                elif propertyName == 'rotation_quaternion':
                    print('Bone:',boneName,' Property:transform.rotation_quaternion')
                elif propertyName == 'scale':
                    print('Bone:',boneName,' Property:transform.scale')
                else:
                    print('Unknown property:',propertyName)
            else:
                print('Parse skeleton property error!')
        else:
            print('Parse skeleton bone name error!')
    else:
        print("Unknown data path!")
    print('Array index:',curve.array_index,' ',end='')
    print('')
    for keyFrame in curve.keyframe_points:
      print('Key time:',keyFrame.co[0],' ',end='')
      print('Key value:',keyFrame.co[1],' ',end='')
      print('')
#
def ShowAnimationInfo(obj):
  if obj.animation_data == None:
    print("No animation data in object!")
    return
  #Check actived action...
  actionCon = 0
  if obj.animation_data.action != None:
    ShowActionInfo(obj.animation_data.action)
    actionCon+=1
  #Check action in NLA tracks...
  for track in obj.animation_data.nla_tracks:
    for strip in track.strips:
      ShowActionInfo(strip.action)
      actionCon +=1
  print("Animation amount:",actionCon )
#
tagObj = bpy.data.objects['metarig']
ShowAnimationInfo(tagObj )

可以看到ShowActionInfo()裡多了非常多的if判斷,目前沒找到比較簡易的寫法,目前的作法以直觀與好懂為原則, 所以程式碼相當長。Bone的名稱如之前所說由於編輯器可以相當自由命名,所以用rfind()來找,property的部分也 是使用rfind()來找也請注意。

參考資料

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(7)

2018年8月27日 星期一

從Blender裡萃取模組資料(7)

從Blender裡萃取模組資料(7)

前言

  續前篇從Blender裡萃取模組資料(6),這次說明動畫的資料如何萃取,在此做個紀錄。

內容

  在開始前,先看看下圖
Blender的動畫資料
在圖中,我將動畫的狀況分為3種,沒有動畫、只有一個動畫與多個動畫這3種,在"沒有動畫"時,物件底下不會有Animation的node,在"只有一個動畫"時,Animation的node底下會有一個"CubeAction"的node,這個node就是啟用中(Actived)的動畫的資料,再來就是"多個動畫"時會發現底下多了個"NLA Tracks"的node,把整個tree展開,會發現底下會多一個"MultiAnimationCubeAction",這個就是非啟用中的動畫資料,如果有多筆非啟用中的動畫的話,"NLA Tracks"的node下就多有多筆"Action Stash"。接著,來看看動畫的內容是如何存放的,如下圖
Blender的動畫內容資料
可以看到很多個Curve,可以想像每個Curve其實就是一個"float"的資料,所以"Location"的動畫被切成3個Curve,再來會看到右側有一些"菱形",而每個"菱形"就代表一個Keyframe,裡面紀錄的是時間與數值。

  接著就來萃取動畫的資料,範例如下
import bpy

def ShowActionInfo(act):
  print('animation name',act.name)
  for curve in act.fcurves:
    print('Data path:',curve.data_path,' ',end='')
    print('Array index:',curve.array_index,' ',end='')
    print('')
    for keyFrame in curve.keyframe_points:
      print('Key time:',keyFrame.co[0],' ',end='')
      print('Key value:',keyFrame.co[1],' ',end='')
      print('')
#
def ShowAnimationInfo(obj):
  if obj.animation_data == None:
    print("No animation data in object!")
    return
  #Check actived action...
  actionCon = 0
  if obj.animation_data.action != None:
    ShowActionInfo(obj.animation_data.action)
    actionCon+=1
  #Check action in NLA tracks...
  for track in obj.animation_data.nla_tracks:
    for strip in track.strips:
      ShowActionInfo(strip.action)
      actionCon +=1
  print("Animation amount:",actionCon )
#
tagObj = bpy.data.objects['Cube']
ShowAnimationInfo(tagObj )

在ShowAnimationInfo()的部分,只要是在辨識動畫的數量,要注意一點的是啟用中的動畫可能會是None,在編輯器編輯時是可以將它調成空的,ShowActionInfo()的部分,有"Data path"與"Array index"兩個資料,"Data path"可能會是"location"、"rotation_euler"或"scale"...等,"Array index"則是個數值,假設得到的結果為"location"與"1",也就是location[1],進一步說就是location.y,請依此類推,"Key time"舊式下方時間條的數值,"Key value"就是數值。

參考資料

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(6)
從Blender裡萃取模組資料(8)

2018年8月20日 星期一

從Blender裡萃取模組資料(6)

從Blender裡萃取模組資料(6)

前言

  續前篇從Blender裡萃取模組資料(5),這次來說明SkinedMesh的權重資料如何取得,在此做個紀錄。

內容

  SkinedMesh的權重資料指的是什麼呢?可以參考下圖
SkinedMesh的權重資料
在SkinedMesh裡的每個Vertex都會有個別的權重資料,裡面會記錄那些Bone對該Vertex的引響力,通常小於1並且不會是負數,範例圖可以看到該Vertex有兩個Bone對它有引響力,分別是"Bone"與"Bone.002",而數值分別為"0.906"與"0.235"。

  接著,就來看如何取得權重資料,範例程式如下
import bpy

tagMesh = bpy.data.meshes["Cube"]
#Create bmesh from mesh
tagBMesh = bmesh.new()
tagBMesh.from_mesh(tagMesh)
#
tagBMesh.verts.ensure_lookup_table()
layer_deform = tagBMesh.verts.layers.deform.active

#Start extract
baseIndex = 0
for face in tagBMesh.faces:
  for i in range(len(face.loops) ):
    print("Position:",face.loops[i].vert.co," ",end="")
    print("Normal:",face.normal," ",end="")
    if layer_deform!=None:
      vertData = tagBMesh.verts[face.loops[i].vert.index]
      weightKeyList = vertData[layer_deform].keys()
      for weightKey in weightKeyList:
        print("WeightIndex:",weightKey," WeightValue:",vertData[layer_deform][weightKey]," ",end="")
    
  print("Loop vertex end")
  loopLen = len(face.loops)
  for i in range( len(face.loops) - 2):
    print("index0:",baseIndex," ",end="")
    print("index1:",baseIndex + i + 1," ",end="")
    print("index2:",baseIndex + i + 2," ",end="")
  print("Loop index end")
  
#Free bmesh...
tagBMesh.free()

這個範例相似於從Blender裡萃取模組資料(1)的範例,但這次會去拿權重的資料。在開頭新增了取的"layer_deform",如果這個值是None表示這個Mesh沒有權重資料,取得權重的部分可以看到事先取得Vertex的資料後再取得權重資料,跟之前的"位置"與"Normal"並不一樣,這一點要注意,再來是取得"weightKeyList",在於List的Key來取得權重數值,這個部分有點特別,"weightKey"並不是Bone的名稱,而只是一個數字!這個數字其實是"Armature.bones"的陣列索引 ,如果忘記"Armature.bones"可以參考從Blender裡萃取模組資料(4)。所以如果要得到名稱的話還需要搭配Armature的資料才可以取得。權重數值的部分取出來就會跟編輯器看到的是一樣的不需要再處理。

參考內容

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(5)
從Blender裡萃取模組資料(7)

2018年8月13日 星期一

從Blender裡萃取模組資料(5)

從Blender裡萃取模組資料(5)

前言

  續前篇從Blender裡萃取模組資料(4),這次會說明Skeleton的transform如何取得,在此做個紀錄。

內容

  在上一篇從Blender裡萃取模組資料(4)中提到了Hierarchy的取得,但Skeleton的Bone還必須包含"Position"與"Rotation"的資訊,這次就坐明這兩種資訊如何取得。
  先來看看只考慮"Position"的部分如何取得,範例如下
import bpy

def ShowBonePosition(bone):
  if bone.parent != None:
    boneWorldPos = bone.head_local
    parentBoneWorldPos = bone.parent.head_local
    print(' Bone pos:',boneWorldPos - parentBoneWorldPos,end='')
  else:
    print(' Bone pos:',bone.head_local,end='')

def ShowBoneInfo(bone,levelCon=0):
  for i in range(levelCon):
    print("  ")
  #
  print('Bone name:',bone.name,end='')
  ShowBonePosition(bone)
  print('')
  #
  for childBone in bone.children:
    ShowBoneInfo(childBone ,levelCon+1)

tagObj = bpy.data.objects["metarig"]
if type(tagObj.data) == bpy.types.Armature:
  armatureData = tagObj.data
  rootBoneList = []
  for bone in armatureData.bones:
    if bone.parent == None:
      rootBoneList.append(bone)
  #
  for rootBone in rootBoneList:
    ShowBoneInfo(rootBone)

搭配前一篇的範例,這次新增"ShowBonePosition()",取得的方法並不困難,Bone的"head_local"代表的是世界座標,所以在有Parent的狀況時,要將它轉成本地座標。

  接著來看看"Position"與"Rotation"如何取得,範例如下
import bpy

def ShowBonePostionAndRotation(bone):
  quat = bone.matrix.to_quaternion()
  if bone.parent != None:
    boneWorldPos = bone.head_local
    parentBoneWorldPos = bone.parent.head_local
    localPos=boneWorldPos - parentBoneWorldPos 
    parentRot = bone.parent.matrix_local.to_3x3().to_quaternion()
    invParentRot = parentRot.inverted()
    print('Bone pos:',invParentQuat.to_matrix() * localPos,end='')
  else:
    print(' Bone pos:',bone.head_local,end='')

  print(' Bone rot:',quat,end='')

def ShowBoneInfo(bone,levelCon=0):
  for i in range(levelCon):
    print("  ")
  #
  print('Bone name:',bone.name,end='')
  ShowBonePostionAndRotation(bone)
  print('')
  #
  for childBone in bone.children:
    ShowBoneInfo(childBone ,levelCon+1)

tagObj = bpy.data.objects["metarig"]
if type(tagObj.data) == bpy.types.Armature:
  armatureData = tagObj.data
  rootBoneList = []
  for bone in armatureData.bones:
    if bone.parent == None:
      rootBoneList.append(bone)
  #
  for rootBone in rootBoneList:
    ShowBoneInfo(rootBone)

這次的做法很不一樣,理由很簡單,如果只是單單需要知道Bone的旋轉,利用Bone的matrix直接取得就是Local的旋轉,不需要再轉換,但"Position"是會被Parent的旋轉引響的,所以要透過Bone的matrix_local(這個是世界旋轉)的"逆旋轉"來反算"Position"。

參考資料

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(4)
從Blender裡萃取模組資料(6)

2018年8月6日 星期一

從Blender裡萃取模組資料(4)

從Blender裡萃取模組資料(4)

前言

  續前篇從Blender裡萃取模組資料(3),這次說明Skeleton的Hierarchy如何萃取,在此做個紀錄。

內容

  記得在從Blender裡萃取模組資料(1)時取得"Mesh"的做法嗎?透過"bpy.data.meshes"取得Mesh的資料,但這個方法有個前提是必須知道"Mesh"的名稱,如果要從所屬的物件來取得是如何取?答案是透過"Object.data"這個Property來取得,透過檢查型別或是用"Object.type"來檢查也可以,為什麼要說明這件事呢?因為這次的Skeleton的資料也是用一樣的方式來取得,在Blender的編輯器裡,看到的"Object.data"如下圖
Skeleton的"Object.data"
取得範例程式碼如下
import bpy

tagObj = bpy.data.objects["metarig"]

#check data by type
print('Check object.data use type()')
if type(tagObj.data) == bpy.types.Armature:
  print('Object has armature data')
elif type(tagObj.data) == bpy.types.Mesh:
  print('Object has mesh data')

print('Check object.data use object.type')
if tagObj.type == 'ARMATURE':
  print('Object has armature data')
elif tagObj.type == 'Mesh':
  print('Object has mesh data')

Mesh的資料取得後可以到從Blender裡萃取模組資料(1)裡參考資料如何萃取,這裡就不再贅述。另外,在Blender中,稱Skeleton為"Armature",要找關鍵字請用"Armature"。

  接著,進入正題"Skeleton的Hierarchy如何萃取",取得的方法是透過"Armature.bones"來取得每一個"Bone",而每一個"Bone"皆有parent與children的資訊,所以只要抓Root bone(沒有parent的Bone),再從Root bone的children來萃取整個Hierarchy即可,範例的程式碼如下
import bpy

def ShowBoneInfo(bone,levelCon=0):
  for i in range(levelCon):
    print("  ")
  #
  print('Bone name:',bone.name)
  #
  for childBone in bone.children:
    ShowBoneInfo(childBone ,levelCon+1)
tagObj = bpy.data.objects["metarig"]

if type(tagObj.data) == bpy.types.Armature:
  armatureData = tagObj.data
  rootBoneList = []
  for bone in armatureData.bones:
    if bone.parent == None:
      rootBoneList.append(bone)
  #
  for rootBone in rootBoneList:
    ShowBoneInfo(rootBone)
    

補充說明,"Bone"裡有"name"這個Property,可以取得Bone的名稱。"ShowBoneInfo()"會遞迴出整個Hierarchy,"levelCon"可以知道目前的階層數。

參考資料

Blender Documentation Contents

相關文章

Blender裡萃取模組資料(3)
從Blender裡萃取模組資料(5)

2018年7月30日 星期一

從Blender裡萃取模組資料(3)

從Blender裡萃取模組資料(3)

前言

  續前篇從Blender裡萃取模組資料(2),這次說明物件的Hierarchy如何萃取,在此做個紀錄。

內容
  在Blender設定Hierarchy有3種常用的方法,一個是直接設定物件的Relations的Parent,如下圖
設定Relations的Parent
這個方法我認為是最好的方式,因為這個方法支援當Parent是Bone的狀況,第二種是"Set Parent To",用法很直覺,選取Child物件後再押住"Shift"選取Parent物件,然後按下Ctrl+p,選取需要的Parent選項即可,這方法很直覺而好用,還可以用來綁骨架,但這個方法有個缺陷,就是無法將物件的Parent設定到Bone,操作狀況如下圖
使用"Set Parent To"
接著是第三種方法,使用"Child Of Constraint",操作狀況如下圖
使用"Child Of Constraint"
這個方法雖然也可以達成Hierarchy效果,但有些相當棘手的問題,問題一,在操作狀況圖片可以看到物件的Hierarchy並沒有真正的改變,這會造成匯出模組時有些物件(Child Of Constraint的Parent物件)必須先匯出,問題二,這個方法並不會防止循環Hierarchy或是2個以上的Parent,如果先用第一或第二種方法設定Hierarchy為A物件的Child為B物件,B物件的Child為C物件,接著利用
"Child Of Constraint"將C物件的Parent設成A物件,造成循環Hierarchy且C物件具有2個Parent,Blender並不會阻止這件事的發生,所以這第三種方法就不打算支援了。
  接著就來看看由第一與第二種設定Hierarchy的資料如何萃取,萃取的範例如下
tagObj = bpy.data.objects["Cube"]
#
for obj in tagObj.children:
  print("child name:",obj.name)
#
if obj.parent != Nome:
  if obj.parent_type=='OBJECT':
    print("parent name:",obj.parent.name)
  elif obj.parent_type=='ARMATURE':
    print("parent name:",obj.parent.name)
  elif obj.parent_type=='Bone':  
    print("parent name:",obj.parent.name," bone name:",parent_bone)

只要存取物件的"Children"就可以得到該物件的Child物件,然後可以透過"Parent"來得到Parent物件,唯一要記住的特例是當物件的Parent是Bone的狀況,實際狀況會到萃取骨架資料時再詳細說明。

參考資料

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(2)
從Blender裡萃取模組資料(4)

2018年7月23日 星期一

從Blender裡萃取模組資料(2)

從Blender裡萃取模組資料(2)

前言

  續前篇從Blender裡萃取模組資料(1),這次會說明物件Transform的資料如何萃取,在此做個紀錄。

內容

  萃取Transform的Position與Scale相當容易,直接從object的Property取得即可,但Rotation的部分就不能直接取得了,相看下圖
Blender的旋轉模式
可以在編輯器看到旋轉模式的選項,在不同的旋轉模式要取不同的Property才能正確取到值,如下說明:
1.  Quaternion(WXYZ)模式下,取roation_quaternion
2.  XYZ Euler模式下,取roation_euler
3.  XZY Euler模式下,取roation_euler
4.  YXZ Euler模式下,取roation_euler
5.  YZX Euler模式下,取roation_euler
6.  ZXY Euler模式下,取roation_euler
7.  ZYX Euler模式下,取roation_euler
8.  Axis Angle模式下,取roation_axis_angle

旋轉模式在編輯器的預設值是"XYZ Euler",以我的需求來說,我會希望它是"Quaternion(WXYZ)",所以該怎麼轉換呢?只要將現在的旋轉模式轉到你想要的模式後,再去取對應的Property即可,整個Transform的資料萃取範例如下

import bpy

tagObj = bpy.data.objects["Cube"]
#
print('Object postion:',tagObj.location)

if obj.rotation_mode == 'QUATERNION':
  print('Object ratation(quaternion):',tagObj .rotation_quaternion)
else:
  #save old ratation mode for recover it.
  oldRotationMode = obj.rotation_mode
  #
  obj.rotation_mode = 'QUATERNION'
  print('Object ratation(quaternion):',tagObj .rotation_quaternion)
  #Recover ratation mode
  obj.rotation_mode = oldRotationMode 

#
print('Object scale:',tagObj.scale)

在取得非"Quaternion"時要把舊的模式記住,這只是為了恢復原本的操作模式,如果不介意旋轉模式被更改的話可以直接設成想要模式後取Property即可。

參考資料

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(1)
從Blender裡萃取模組資料(3)