2016年5月15日 星期日

用C++寫2D捲軸射擊遊戲

緣起:
學習C++多年,一直想自己寫個game,近日寫game的background knowledge已趨完整俱備,就寫了一個2D垂直向捲軸射擊game雛型,中文就叫"射擊就對了",這個game已具備2D捲軸射擊基本要素:我機(目前設定是無敵狀態)/敵機/子彈/場景/音效,目前改進的空間還很大,日後會補上計分系統/吃彈藥包等等,大家可以參考這個雛型修改成自己想要的模樣,本篇blog有不足之處,日後補上,請注意引用本篇blog不必註明出處,但程式的部分是參考網路上的資料而修改的,請注意智慧財產權,

程式參考網站:
1.C++實務班(http://garfstudio.blogspot.tw/2015/12/cyoutube.html),
2.DirectX教學網站(http://www.rastertek.com/dx11tut14.html),

程式細節:
1.我機/敵機/子彈:這部分的程式是參考底下這個網站修改而成:
http://garfstudio.blogspot.tw/2015/12/cyoutube.html
這位作者(Gary Lin老師)在youtube有放上完整教學影片_C++程式設計:(https://www.youtube.com/channel/UC6kvAnrYEXwE4tVG8zOdWvw),
建議初學者請先完整看完全部29小時的課程(29部影片),所以這部分程式不多作解釋,

2.背景捲軸:原本從天文網站抓一張星象圖裁成800*600的24-bit BMP無縫圖,每次向上捲動幾個pixel,結果原先只有白背景+我機+敵機+子彈時的fps還有1k~2k,加上背景後只剩4,最後換另一種做法,將背景圖切成10x10的小矩形,由這些小矩形來構成背景圖,然後每次捲動一列,fps可提升到幾百,但目前暫時限制住fps為30左右,

3.音效:由C++實務班的youtube影片中得知簡單播放音樂的函數是playsound(),但無法同時播放兩種以上的音效(這叫混音),在第一個背景音樂(重複播放)播放時,又播放第二個音效檔,會導致第一個音效停止播放,微軟論壇有人問過這個問題:
https://social.msdn.microsoft.com/Forums/vstudio/en-US/de87d08e-dab6-4b4e-948a-3ef4142b7e04/how-could-i-mutiple-sounds-with-playsound?forum=vclanguage,
有人提出Direct Sound(DirectX SDK的一部份)這組api可以做出"混音"效果,先抓微軟官網的DirectX SDK回來安裝,其實只要安裝include(標頭檔)跟Lib即可讓程式編譯,在網路上有找到教學網站(DirectX教學網站:http://www.rastertek.com/dx11tut14.html),拿範例程式(soundclass.h,soundclass.cpp)來改成同時播三個音效,這三個音效檔是windows作業系統內建的,DS1.wav當背景音樂所以設成重複播放,DS2.wav當發射子彈的音效,DS3.wav當子彈打到敵機的音效,

本篇blog附上範例程式的使用方式:
1.在任何一種版本的Visual Studio建立C++ win32專案,專案名稱打"JustShootIt",專案建立後再修改專案->屬性->不使用Unicode字集,專案->屬性->C/C++->先行編譯標頭檔->不使用先行編譯標頭檔,開啟JustShootIt.cpp,清光內容,

2.將範例程式解壓縮,除了JustShootIt.cpp以外,其他檔案都放到專案的那個目錄內,用記事本打開JustShootIt.cpp,全部複製貼到Visual Studio編輯視窗的JustShootIt.cpp裡頭,

3.依序為專案加入game.h,dsound.h,fps_time.h,key_state.h,map.h,soundclass.h,soundclass.cpp,

4.build->Run,

遊戲操作方式:
1.射擊子彈(滑鼠游標跟我機連成一線的方向就是子彈的射出方向):Shift鍵+Z,
2.上下左右方向鍵控制我機的移動方向,

遊戲畫面:

範例程式:
https://drive.google.com/file/d/0B6EVEd5P2B9cVDhIaGFqRVI3OUk/view?usp=sharing

2015年11月21日 星期六

Programming Windows using Win32 api_BMP檔開圖程式

緣由:1.最近因需要而接觸QR碼,網路上找到的sample是存檔成.png,想要自己改成.bmp存檔,因而開始研究BMP圖檔結構,網路上找到的sample是寫成console,試成功後,覺得不過癮,想寫成視窗將BMP圖秀在視窗內,
2.很久以前就想寫一個像ACDsee的秀圖軟體,又可以像photoshop一樣可以做影像處理,最近上C++的課程接觸到用win32 api來寫視窗程式,同時也有接觸到拿視窗當一塊'畫布'來畫的概念,
3.所以就著手開始寫了ezBMP這個秀圖+一些簡單的影像處理特效的軟體!相關程式碼以及開發過程稍微整理一下就寫了這篇blog.
若有不周,歡迎批評指教, 缺漏的部份日後會慢慢補上.轉貼或引用本篇blog內文章段落,不必交代出處.程式碼的部分是參考網路上的程式改寫而成,所以相關著作權法律的問題請注意一下!

步驟:
現在開始進入主題,底下會講解如何新增ezBMP專案,編譯跟執行,程式跑的流程,一邊解釋程式碼,一邊解釋BMP圖檔架構,win32 api如何開發視窗程式等等,
為介紹BMP圖檔簡化起見,ezBMP軟體只能秀24bit BMP圖檔(用windows附屬應用程式->小畫家->另存新檔選24bit bmp),
1.用小畫家先製作寬300,高100的ezBMP_logo.bmp,灰底白字'ezBMP',
2.打開微軟"Visual Studio系列",檔案->新增->專案->選Visual C++->Win32->Win32專案,名稱打上'ezBMP',其它設定都不必更改,按"完成",
 - 專案屬性->"字元集"設成"未設定",C/C++->先行編譯標頭檔->設成"未使用先行編譯標頭檔",
3.方案總管->打開ezBMP.cpp,裡頭已經有一些自動產生的程式碼了:
// ezBMP.cpp : 定義應用程式的進入點。
//
#include "stdafx.h"
#include "ezBMP.h"
#include "bmp.h" //<-----------加入此行,主要的秀圖程式都在裡面,
#define MAX_LOADSTRING 100
// 全域變數:
HINSTANCE hInst;  // 目前執行個體
TCHAR szTitle[MAX_LOADSTRING];// 標題列文字,可去ezBMP.rc的String Table改變,在此改無效!
TCHAR szWindowClass[MAX_LOADSTRING];// 主視窗類別名稱
int image_effect=0; //<--------加入此行,flag指出目前特效狀態,初值zero表示秀出原始圖,
int image_effect2=0;//<--------加入此行,flag指出目前存檔狀態,初值zero表示不存檔,
char szFileName[MAX_PATH] = "D:\\ezBMP\\ezBMP_logo.bmp";//<--------加入此行,開啟的檔名,假設存放的路徑在專案目錄底下"D:\ezBMP",
//移動'開檔'子視窗會造成母視窗(ezBMP本身)重繪,重繪需要讀檔,檔名讀不到就exit(0),開檔當下卡住公用變數szFileName的路徑,
//若是第一次開ezBMP去讀szFileName初值,因為只給檔名,沒給路徑,甚至只給相對路徑,會因為'開檔當下'卡住'路徑',也就是路徑由程式亂決定,
//為控制住這情況需給絕對路徑,這時就不會受影響了,
char szFileName3[MAX_PATH] = "output.bmp";//<--------加入此行,存檔的檔名,
// 這個程式碼模組中所包含之函式的向前宣告:
ATOM    MyRegisterClass(HINSTANCE hInstance);
BOOL    InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
 ...
}
//
//  函式: MyRegisterClass()
//  用途: 註冊視窗類別。
//  註解:
//    只有當您希望此程式碼能相容比 Windows 95 的
//    'RegisterClassEx' 函式更早的 Win32 系統時,
//    才會需要加入及使用這個函式。
//    您必須呼叫這個函式,讓應用程式取得與它相關的
//    「格式正確」的圖示。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
 ...
}
//
//   函式: InitInstance(HINSTANCE, int)
//   用途: 儲存執行個體控制代碼並且建立主視窗
//   註解:
//        在這個函式中,我們會將執行個體控制代碼儲存在全域變數中,
//        並且建立和顯示主程式視窗。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
 ...
   ShowWindow(hWnd, SW_MAXIMIZE);//<---此行改成這樣,ezBMP一執行馬上變全螢幕,
 ...
}
//
//  函式: WndProc(HWND, UINT, WPARAM, LPARAM)
//  用途:  處理主視窗的訊息。
//  WM_COMMAND - 處理應用程式功能表
//  WM_PAINT - 繪製主視窗
//  WM_DESTROY - 顯示結束訊息然後返回
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 int wmId, wmEvent;
 PAINTSTRUCT ps;
 HDC hdc;
     TCHAR greeting[] = _T("24bit(original image)");//<-----加入此行,圖上方秀標題,
     HMENU hMenu;//<-----加入此行,改變menu某些item變灰色,無法點選,
 switch (message)
 {
  case WM_COMMAND:
   wmId    = LOWORD(wParam);
   wmEvent = HIWORD(wParam);
   // 剖析功能表選取項目:
   switch (wmId)
   {
    case IDM_ABOUT:
     DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
     break;
//底下開始要加入這些code,從case ID_Gray到case ID_SaveFile,
//另外方案總管旁邊有'資源檢視'打開Menu的IDC_EZBMP,編輯視窗內只有'檔案','說明',旁邊打字的地方打'影像特效',
//它的下方打'&Gray',多打'&',會變成G底下有畫線,變成有熱鍵的功能,依序增加'&BlackWhite','&Sobel Operator',每增加一個,屬性ID也要改,
//例如打了'&Gray',ID要改成ID_Gray,這個就是執行ezBMP後,若有人點了menu的'Gray',系統就會送ID_Gray給ezBMP,進到WndProc()的這段程式來執行,
//這邊不做處理,只是記錄一個flag(image_effect),然後呼叫InvalidateRect(),跟系統宣告這塊client area(menu正下方的白色區塊)為無效區,
//系統就會傳送訊息'WM_PAINT'給ezBMP進到WndProc()的case WM_PAINT:這段程是來執行畫圖(將此區塊"重繪"),
//InvalidateRect()第三個參數TRUE,代表重新繪製前要先清空該區域的圖,
//點menu'檔案',其下方也要依序增加'&Header Info','&Open File','&Save File',
           case ID_Gray:
                image_effect=1;
     InvalidateRect(hWnd,NULL,TRUE);//3rd parameter:bErase = TRUE,clean the screen first,
     break;
    case ID_BlackWhite:
     image_effect=2;
     InvalidateRect(hWnd,NULL,TRUE);
     break;
    case ID_SobelOperator:
     image_effect=3;
     InvalidateRect(hWnd,NULL,TRUE);
     break;
//秀檔頭資訊,每個BMP圖檔,檔頭一開始的54 bytes有存一些資訊,例如:圖形的寬度,高度,color depth等等,
    case ID_headerinfo:
     image_effect=4;
     InvalidateRect(hWnd,NULL,TRUE);
     break;
//'開啟圖檔'的界面視窗(可讓人選哪目錄的哪圖檔)是靠將ofn這個structure傳入GetOpenFileName()來達成,
//先include Commdlg.h才能使用這個structure,可以在stdafx.h內打入:#include <Commdlg.h>
//ZeroMemory()可將ofn結構的所有成員都設Null,避免有些成員初值沒給到就變unknown,
//若有人按'取消',不執行if那段程式,選定某圖檔再按'開啟',則會將此檔名連同路徑存到szFileName,
//將szFileName設為公用變數,另外一個檔案bmp.h,可藉由宣告此變數為extern,則bmp.h內所有函數可以存取此公用變數,
    case ID_OpenFile:
     OPENFILENAME ofn;
     ZeroMemory(&ofn, sizeof(ofn));
     ofn.lStructSize = sizeof(ofn);
                ofn.hwndOwner = hWnd;
                ofn.lpstrFilter = "BMP Files (*.bmp)\0*.bmp\0All Files (*.*)\0*.*\0";
                ofn.lpstrFile = szFileName;
                ofn.nMaxFile = MAX_PATH;
                ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
                ofn.lpstrDefExt = "bmp";
     if(GetOpenFileName(&ofn))
     {
      image_effect=0;
      InvalidateRect(hWnd,NULL,TRUE);
     }
     break;
//'存檔'功能是可將畫面秀的影像特效存成檔案,不共用flag(image_effect)的原因是存檔前需靠getPixel函數取得畫面某(x,y)座標的RGB值,
//所以要先秀原先特效圖,才能GetPixel(),然後再寫檔,
    case ID_SaveFile:
     ZeroMemory(&ofn, sizeof(ofn));
                ofn.lStructSize = sizeof(ofn);
                ofn.hwndOwner = hWnd;
                ofn.lpstrFilter = "BMP Files (*.bmp)\0*.bmp\0All Files (*.*)\0*.*\0";
                ofn.lpstrFile = szFileName3;
                ofn.nMaxFile = MAX_PATH;
                ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
                ofn.lpstrDefExt = "bmp";
                if(GetSaveFileName(&ofn))
                {
      image_effect2=1;
      InvalidateRect(hWnd,NULL,TRUE);
     }
     break;
    case IDM_EXIT:
     DestroyWindow(hWnd);
     break;
    default:
     return DefWindowProc(hWnd, message, wParam, lParam);
   }
   break;
//因為有時要將menu某些item變成灰色,使得無法點選,例如畫面秀文字headerinfo時無法存檔,開啟原始圖也無法存檔,
//menu變成灰色需要hMenu,hWnd,所以要將hWnd,hMenu也傳進bmpDraw()內,hdc是畫布,所以一定要傳進去,
  case WM_PAINT:
   hMenu = GetMenu(hWnd);//<----加入此行,
   hdc = BeginPaint(hWnd, &ps);
   bmpDraw (hWnd,hdc,hMenu) ;//<----加入此行,
   EndPaint(hWnd, &ps);
   break;
  case WM_DESTROY:
   PostQuitMessage(0);
   break;
  default:
   return DefWindowProc(hWnd, message, wParam, lParam);
 }
 return 0;
}
// [關於] 方塊的訊息處理常式。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
 UNREFERENCED_PARAMETER(lParam);
 switch (message)
 {
 case WM_INITDIALOG:
  return (INT_PTR)TRUE;
 case WM_COMMAND:
  if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
  {
   EndDialog(hDlg, LOWORD(wParam));
   return (INT_PTR)TRUE;
  }
  break;
 }
 return (INT_PTR)FALSE;
}
---
新增bmp.h:
方案總管->原始程式檔->加入->新增項目->Visual C++->標頭檔.h->輸入名稱'bmp',將底下的程式貼上去,
#include "stdafx.h"
#include <iostream> //load the iostream libery
#include <sstream>
using namespace std;
#include <fstream>  //load the fstream libery to write and read the files.
using std::fstream;
using std::ios;
struct BmpHead{
 unsigned short bfType;          //2 Identifier 'BM'
 unsigned long  bfSize;          //4 File size
 unsigned long  bfReserved;      //4 Reserved
 unsigned long  bfOffBits;       //4 Bitmap data offset
 unsigned long  biSize;          //4 Bit map header size
 unsigned long  biWidth;         //4 Width
 unsigned long  biHeight;        //4 Height
 unsigned short biPlanes;        //2 Planes
 unsigned short biBitCount;      //2 Bits per pixel(1,4,8,16,24,32)
 unsigned long  biCompression;   //4 Compression
 unsigned long  biSizeImage;     //4 Bitmap data size
 unsigned long  biXPelsPerMeter; //4 HResolution
 unsigned long  biYPelsPerMeter; //4 VResolution
 unsigned long  biClrUsed;       //4 Colors
 unsigned long  biClrImportant;  //4 Important colors
};
// declare some function
BmpHead* ReadBmpHeader(char* file); // read the header of bmp picture
unsigned char* ReadImage(char* file, int ImageSize, int bfOffBits); // read the raw data of bmp picture
void WriteBmpHeader(char *file, BmpHead* bmHead); // write the header of bmp picture
void WriteImage(char* file,int ImageSize, int bfOffBits, unsigned char* raw_data); // write the raw data of bmp picture
void PrintBmpHeader(HDC hdc,BmpHead* pBmpHeader); // print the information of bmp header of image
void showOriginBMP(HDC hdc);
void showGrayBMP(HDC hdc);
void showBlackWhiteBMP(HDC hdc);
void showSobelBMP(HDC hdc);
void PrintFileName(HDC hdc);
unsigned char* GetImage(HDC hdc, int totalSize);
BmpHead* pBmpHeader;
unsigned char* pucImageData;//the raw data of original bmp picture
int totalSize; //total size of raw data
int **pGrayRawData, *pData;
extern int image_effect;
extern int image_effect2;
extern char szFileName[MAX_PATH];
extern char szFileName3[MAX_PATH];
void bmpDraw(HWND hWnd,HDC hdc,HMENU hMenu)
{
//BMP圖檔,檔頭開始的前54bytes為檔頭資訊,依序讀入到pBmpHeader的結構成員中,
//biBitCount(color depth):24bit也就是Bits per pixel,每個pixel用多少bits記錄r,g,b值,
//24bits=3 bytes,r:1byte,g:1byte,b:1byte,
//24bits的BMP圖檔沒有調色盤,也就是54bytes檔頭資訊後面緊接著raw data(每一個pixel的rgb值),
//raw data有幾個byte?就是width*height*3,寬度跟高度的單位為pixel,
//若高度為正,則緊接著raw data的第一組3bytes,就是一張圖的四個端點(左上,左下,右上,右下)的左下那個pixel的rgb值,
//然後就是水平掃描線(從左邊掃到右邊),由圖片的最底下那條水平線開始掃,往上掃上去,檔尾最後一組3bytes就是整張圖片右上角pixel的rgb,
 pBmpHeader = ReadBmpHeader(szFileName);
 totalSize = (pBmpHeader ->biHeight)*(pBmpHeader ->biWidth)*3;
 pucImageData = ReadImage(szFileName, totalSize, pBmpHeader->bfOffBits);
 if(image_effect==0)
  showOriginBMP(hdc);//秀原始圖,
 else if(image_effect==1)
         showGrayBMP(hdc);//秀灰階圖,
     else if(image_effect==2)
         showBlackWhiteBMP(hdc);//秀黑白圖,
     else if(image_effect==3)
         showSobelBMP(hdc);//秀Sobel Operator(邊緣偵測),
     else if(image_effect==4)
         PrintBmpHeader(hdc,pBmpHeader);//秀檔頭資訊,
 if(image_effect2==1) //存檔,
 {
  WriteBmpHeader(szFileName3, pBmpHeader);//先寫入檔頭,
         pucImageData = GetImage(hdc, totalSize);//先抓取目前畫面上秀的特效圖的rgb值,
         WriteImage(szFileName3, totalSize, pBmpHeader->bfOffBits,pucImageData);//寫入raw data,
         PrintFileName(hdc);//印出the file is saved!字樣,
         image_effect2=0;//回復flag初值,避免二度存檔,
     }
     if(image_effect==0||image_effect==4)
         EnableMenuItem(hMenu,ID_SaveFile,MF_GRAYED);//目前畫面秀原始圖或檔頭資訊時,無法按存擋,
     else
         EnableMenuItem(hMenu,ID_SaveFile,MF_ENABLED);
     SetMenu(hWnd,hMenu);
}
//秀原始圖,在畫布上要寫字,就用TextOut(),
//在畫布上要畫圖,其實是描點,就用SetPixel,給定座標(x,y),rgb值就能在畫布的(x,y)用不同顏色描點,
//raw data沒給座標值,座標在剛剛上面的敘述已經說過了,
//要注意一點的是,r,g,b三個byte的高低順序,r>b>g,但存成圖檔時是low byte先存,
void showOriginBMP(HDC hdc)
{
    TCHAR greeting[] = _T("24bit(original image)");
    SetTextColor(hdc,RGB(0, 0, 0));//設定前景文字顏色
    SetBkColor(hdc, RGB(255, 0, 0));//設定背景色
    SetBkMode(hdc, OPAQUE);//OPAQUE有背景色,TRANSPARENT無背景色,
    TextOut(hdc, 5, 5, greeting, _tcslen(greeting));
    int x=5+pBmpHeader->biWidth,y=22;//raw data我從檔尾最末一個pixel開始描點,raw data都存在一個array((pucImageData[]),
    for(int i=totalSize-1;i>=0;i--){
        SetPixel(hdc,x,y,RGB(pucImageData[i],pucImageData[i-1],pucImageData[i-2]));//low byte first,
        i-=2;
        x--;
        if(i%((pBmpHeader->biWidth)*3)==0){//每掃描完一條水平線,x座標歸位到Max(最右邊),y座標往下加1,
            x=5+pBmpHeader->biWidth;
            y++;
        }
    }
}
//秀灰階圖,將r,g,b拿來再做運算,得到一個值,描點時r=g=b=那個值,
void showGrayBMP(HDC hdc)
{
    TCHAR greeting[] = _T("gray image");
    SetTextColor(hdc,RGB(0, 0, 0));
    SetBkColor(hdc, RGB(255, 0, 0));
    SetBkMode(hdc, OPAQUE);//OPAQUE有背景色,TRANSPARENT無背景色,
    TextOut(hdc, 5, 5, greeting, _tcslen(greeting));
    int x=5+pBmpHeader->biWidth,y=22;
    for(int i=totalSize-1;i>=0;i--){
        unsigned char gray;
        gray=(unsigned char)(0.299*pucImageData[i] + 0.587*pucImageData[i-1] + 0.114*pucImageData[i-2]);
        SetPixel(hdc,x,y,RGB(gray,gray,gray));
        i-=2;
        x--;
        if(i%((pBmpHeader->biWidth)*3)==0){
            x=5+pBmpHeader->biWidth;
            y++;
        }
    }
}
//秀黑白圖,將r,g,b拿來再做運算,得到一個值,那個值超過某一門檻就設為白色,沒超過就設為黑色,描點時r=g=b=黑或白,
void showBlackWhiteBMP(HDC hdc)
{
    TCHAR greeting[] = _T("black-white image");
    SetTextColor(hdc,RGB(0, 0, 0));
    SetBkColor(hdc, RGB(255, 0, 0));
    SetBkMode(hdc, OPAQUE);//OPAQUE有背景色,TRANSPARENT無背景色,
    TextOut(hdc, 5, 5, greeting, _tcslen(greeting));
    int x=5+pBmpHeader->biWidth,y=22;
    for(int i=totalSize-1;i>=0;i--){
        unsigned char gray;
        gray=(unsigned char)(0.299*pucImageData[i] + 0.587*pucImageData[i-1] + 0.114*pucImageData[i-2]);
        if (gray>200) gray=255;
        else gray=0;
        SetPixel(hdc,x,y,RGB(gray,gray,gray));
        i-=2;
        x--;
        if(i%((pBmpHeader->biWidth)*3)==0){
            x=5+pBmpHeader->biWidth;
            y++;
        }
    }
}
//sobel operator步驟比較複雜一點,需對這張圖每個pixel鄰近8個點,也就是連自己本身這個pixel也算的九宮格做運算,
//先把存raw data的一維矩陣pucImageData[i]轉換成二維矩陣pGrayRawData[h][w],對應到平面座標(w,h),這樣鄰近的pixel比較好取出做運算,
//pGrayRawData因無法事先知道陣列大小,所以用動態矩陣的方式來寫程式,邊轉換就已經邊轉灰階了,
void showSobelBMP(HDC hdc)
{
    int i,Gz,Gx,Gy,A,B,C,D,E,F,G,H,I;//[A B C;D E F;G H I]
    TCHAR greeting[] = _T("Image after Sobel Operator");
    SetTextColor(hdc,RGB(0, 0, 0));
    SetBkColor(hdc, RGB(255, 0, 0));
    SetBkMode(hdc, OPAQUE);//OPAQUE有背景色,TRANSPARENT無背景色,
    TextOut(hdc, 5, 5, greeting, _tcslen(greeting));
    pGrayRawData = (int **)malloc((pBmpHeader ->biHeight)*sizeof(int *)+(pBmpHeader ->biHeight)*(pBmpHeader ->biWidth)*sizeof(int));
    for (i = 0, pData = (int *)(pGrayRawData+(pBmpHeader ->biHeight)); i < (pBmpHeader ->biHeight); i++, pData += (pBmpHeader ->biWidth))
        pGrayRawData[i]=pData;
    i=totalSize-1;
    for (int h=0;h<pBmpHeader ->biHeight;h++)
        for(int w=pBmpHeader ->biWidth-1;w>=0;w--)
        {
            unsigned char gray;
            gray=(unsigned char)(0.299*pucImageData[i] + 0.587*pucImageData[i-1] + 0.114*pucImageData[i-2]);
            pGrayRawData[h][w]=gray;
            i-=3;
        }
//正式做sobel operator運算,公式查wiki就有,因不知鄰近的點取不取的到(取不到的意思是超出這張圖的範圍,位於圖邊界的pixel就會發生這問題),
//所以先假設未知數A,B,C,D,E,F,G,H,I,位於中央的點就是E,E本來就位於這張圖範圍,所以一定存在,就不需if判斷式,其他若判斷取不到就給值zero,
    for (int h=0;h<pBmpHeader ->biHeight;h++)
        for(int w=pBmpHeader ->biWidth-1;w>=0;w--)
        {
            if((w-1)<0) D=0;// in case array idx not illegal,
            else D=pGrayRawData[h][w-1];
           
            if(w+1>=pBmpHeader ->biWidth) F=0;
            else F=pGrayRawData[h][w+1];
           
            if(h-1<0) B=0;
            else B=pGrayRawData[h-1][w];
           
            if(h+1>=pBmpHeader ->biHeight) H=0;
            else H=pGrayRawData[h+1][w];
            if((h-1<0)||(w-1<0)) A=0;
            else A=pGrayRawData[h-1][w-1];
            if((h-1<0)||(w+1>=pBmpHeader ->biWidth)) C=0;
            else C=pGrayRawData[h-1][w+1];
            if((h+1>=pBmpHeader ->biHeight)||(w-1<0)) G=0;
            else G=pGrayRawData[h+1][w-1];
            if((h+1>=pBmpHeader ->biHeight)||(w+1>=pBmpHeader ->biWidth)) I=0;
            else I=pGrayRawData[h+1][w+1];
            E=pGrayRawData[h][w];
            Gx=(1*A)+(0*B)+((-1)*C)+(2*D)+(0*E)+((-2)*F)+(1*G)+(0*H)+((-1)*I);//rotating the mask 180 degree not cause the different output,
           
            Gy=(1*A)+(2*B)+(1*C)+(0*D)+(0*E)+(0*F)+((-1)*G)+((-2)*H)+((-1)*I);
           
            Gz=abs(Gy)+abs(Gx);
            if (Gz>255) Gz=255;//Gz always >0,but maybe > 255,
            SetPixel(hdc,5+w,22+h,RGB(Gz,Gz,Gz));
        }
    free(pGrayRawData);
}
//存檔後,印出檔名以及file is saved,
void PrintFileName(HDC hdc)
{
...
}
//讀取BMP圖檔的檔頭資訊,存到structure pBmpHeader,
BmpHead* ReadBmpHeader(char *file)
{
 ...
}
//讀取BMP圖檔的raw data,
unsigned char* ReadImage(char* file, int totalSize, int bfOffBits)
{
 ...
}
//讀取目前畫面上特效圖每個pixel的rgb值,因為特效都是灰階圖,所以r=g=b,
//GetPixel得到rgb三個值,用GetRValue取出r的值,
unsigned char* GetImage(HDC hdc, int totalSize)
{
    int r;
 unsigned char* raw_data;
 raw_data = new unsigned char[totalSize];
    int x=5+pBmpHeader->biWidth,y=22;
    for(int i=totalSize-1;i>=0;i--){
        r=GetRValue(GetPixel(hdc,x,y));
        raw_data[i]=r;
        raw_data[i-1]=r;
        raw_data[i-2]=r;
        i-=2;
        x--;
        if(i%((pBmpHeader->biWidth)*3)==0){
            x=5+pBmpHeader->biWidth;
            y++;
        }
    }
    return raw_data;
}
//存檔時寫入檔頭資訊,
void WriteBmpHeader(char *file, BmpHead* bmHead)
{
    ...
}
//存檔時寫入raw data,
void WriteImage(char* file,int totalSize, int bfOffBits, unsigned char* raw_data)
{
 ...
}
//在畫面上秀'檔頭資訊',比較麻煩的是TextOut函數第四個參數要TCHAR類別的變數,
//所以印固定的字(例如:'TYPE : ')用string類別要先轉char再轉TCHAR類別,
//int類別的要先轉char再轉TCHAR,
void PrintBmpHeader(HDC hdc,BmpHead* pBmpHeader)
{
    TCHAR greeting[] = _T("Header Info of the pic");
    SetTextColor(hdc,RGB(0, 0, 0));
    SetBkColor(hdc, RGB(255, 0, 0));
    SetBkMode(hdc, OPAQUE);//OPAQUE有背景色,TRANSPARENT無背景色,
    TextOut(hdc, 5, 5, greeting, _tcslen(greeting));
    SetBkMode(hdc, TRANSPARENT);//OPAQUE有背景色,TRANSPARENT無背景色,
    //int轉char
    int i = pBmpHeader->bfType;
    char buffer[80];
    sprintf(buffer, "%d", i);
    //char轉TCHAR
    TCHAR Name[100];
    sprintf(Name, "%s", buffer);    
    TextOut(hdc, 55, 20, Name, _tcslen(Name));
    //string轉char,char轉TCHAR
    char buffer2[80];
    string type="TYPE : ";
    strcpy(buffer2, type.c_str());
    sprintf(Name, "%s", buffer2);    
    TextOut(hdc, 5, 20, Name, _tcslen(Name));
...
}
測試:
先去底下這網站抓Neighborhood_watch_color.jpg,用小畫家打開另存新檔(24bit .bmp),
https://zh.wikipedia.org/wiki/%E7%81%B0%E5%BA%A6%E5%9B%BE%E5%83%8F
1.執行ezBMP,如下圖:
2.開啟剛用小畫家存的圖Neighborhood_watch_color.bmp,如下圖:
3.點選影像特效的gray,如下圖:
4.點選影像特效的blackwhite,如下圖:
5.點選影像特效的sobel operator,如下圖:
6.將此影像特效存檔,如下圖:
7.點選'檔案'->'Header info'如下圖,不知為何,經過小畫家轉存BMP檔後,就遺失raw data size:
2018-08-26更新:
新增範例程式載點(使用方法請參考"用C#寫crawler(網路爬蟲)"這篇)
https://drive.google.com/file/d/1aUF9-7O3SDgg0f_Fioskj9P9I-Cpn20k/view?usp=sharing

2018-09-24更新:
增進秀圖時間縮短(瞬間),範例程式載點(使用方法請參考"用C#寫crawler(網路爬蟲)"這篇)
https://drive.google.com/file/d/15ndqRg12audn0xKwHQnMRLLLun-ddxFw/view?usp=sharing

2018-09-30更新:
新增按鍵"+"跟按鍵"-"可調整特效(灰階,blackwhite)參數,範例程式載點:
https://drive.google.com/file/d/16bgBnkpQSWtO-vj-2pYgCkITDlRQ_c7X/view?usp=sharing

2018-11-18更新:
新增"編輯"的Ctrl+z(UnDo,復原)/Ctrl+y(ReDo,重做),同時使用GDI+ Bitmap類別的LockBits/UnlockBits方法取代SetPixel/GetPixel函數,藉以提升程式效能,範例程式載點:
https://drive.google.com/file/d/1VVcq-Gvbc7eU3rHa6q67GsNyRJWb8BSL/view?usp=sharing

2018-11-24更新
新增特效a. pencil sketch(鉛筆畫), b.Smoothing平滑化,範例程式載點:
https://drive.google.com/file/d/1jpXTVL3s9Kl7PwrnBmBhd_32i3ksg9pG/view?usp=sharing

2018-12-08更新
除了原來的BMP格式圖檔,新增GIF, JPEG, PNG, TIFF,範例程式載點:
https://drive.google.com/file/d/1ktoKBRapTXrrOSnZXQ48EDST2-3YgCpu/view?usp=sharing

2019-01-06更新
新增statusbar秀檔名,範例程式載點:
https://drive.google.com/file/d/1Pw7x2_vAI9h0_UHGwvA4PGbl2VocKkEr/view?usp=sharing

2019-02-28更新
新增scrollbar支援超過螢幕的大圖, 同時對"二值化&灰階特效"改善效能,範例程式載點:
https://drive.google.com/file/d/1d8lnInimZDzV5j9SJmxpSMMk_UWJy6tM/view?usp=sharing

2014年11月16日 星期日

9軸感應器第二彈_重力感應器ADXL345

前言:
日前從網路上購得9軸感應器GY-80, 搭配TI MSP430 Launchpad寫有關重力加速度計ADXL345程式下去實測. 若有不周,歡迎批評指教,缺漏的部份日後會慢慢補上. 轉貼或引用本篇blog內文章段落, 不必交代出處.程式碼的部分是參考網路上arduino的程式改寫成適合MSP430跑的程式,用CCSv6編譯而成,所以相關著作權法律的問題請注意一下!

說明:
高中物理教的靜止狀態下_重力加速度約1g, GY-80板子背後標示X-Y-Z軸方向就是ADXL345的X-Y-Z軸的方向. 加速計能測各種使板子產生加速度的力,含靜止時只受重力. 等下介紹靜止狀態下, 3軸各別只有單一軸受到重力量到的值, 板子測法跟上一篇介紹的一樣.

本板使用i2c通訊:
Vcc(3.3V接launchpad的pin1),
GND(接launchpad的pinpin20 GND),
SCL(接launchpad的P1.6),
SDA(接launchpad的P1.7).

讀值採16bit的2的補數, 程式中直接將X軸,Y軸,Z軸讀值16bit每個bit都秀在HW uart(9600bps)那邊,1分鐘讀一次, 16bit的數值(單位:LSB)依2的補數還原成10進位的數字(不會算的可以用小算盤來算)後還要乘上4mg/LSB(單位:毫g,10的負三次方g),實際測試數據如下:

只有z軸受力(單位:g,所以正常值應該接近1):
1111111111101011         1111111111110010                 00000000 11011000
x                                       y(-14*4/1000=-0.056g)            z(216*4/1000=0.864g)
----------------------------------------------------------------------------------------
只有y軸受力(單位:g,所以正常值應該接近1):
0000000000000010         0000000011110100                 1111111111011000
x                                       y(244*4/1000=0.976g)            z(-40*4/1000=-0.16g) 
----------------------------------------------------------------------------------------
只有x軸受力(單位:g,所以正常值應該接近1):
0000000100000000         1111111111101000                 1111111111100111
x(256*4/1000=1.024g)    y(-24*4/1000=-0.096g)           z

所有程式碼下載連結:
https://drive.google.com/file/d/0B6EVEd5P2B9ceHdYSGZYMkF4dkU/view?usp=sharing

2014年10月19日 星期日

Acer Iconia Tab B1-A71重力感應器校正

前言:
最近聽到有人在study 9軸感應器(陀螺儀+加速計+磁力計),也想弄一個來玩玩看!目前先研究手邊剛好有的東西(Acer Iconia Tab B1-A71), 寫程式下去查的結果,這台只有G-sensor(重力感應器或稱加速計). 若有不周,歡迎批評指教,缺漏的部份日後會慢慢補上. 轉貼或引用本篇blog內文章段落, 不必交代出處.不過程式碼的部分是拿網路上某人寫的Android sample code而寫成的,所以相關著作權法律的問題請注意一下!

過程:
在網上有看到一篇Acer在澳洲官網的技術支援網頁刊登如何校正Iconia Tab B1-A71的重力感應器, 試了一下ok, 步驟如下:
1.設定->關於平板電腦,

2.點一下"裝置資訊",

3.在"重力感應器驅動程式版本"上連續點10下(就是一直點到畫面變到下一張圖為止),
 
4.按下"重力校準"依照指示開始校正,

 

說明:
高中物理教的重力加速度約9.8m/s^2(平方表示為^2,就是米/秒的平方),面板就是X-Y軸構成的平面, Z軸是X軸跟Y軸的向量外積(X close Y)後的方向. 加速計能測各種使平板產生加速度的力,含靜止時只受重力.底下示範靜止受重力, 3軸量到的值,下圖表示左手提著平板,在Y軸方向受到一個重力,在X軸跟Z軸沒受到任何重力, 所以得到Y值就是9.8 m/s^2, 其他兩軸約為0 m/s^2.
 
同理可證,照下圖這種拿法,在X軸受到一個重力,在Y軸跟Z軸方向沒有受到任何重力,所以X值就是9.8 m/s^2, Y值跟Z值都是0 m/s^2,
 
將平板平放在桌面則會讓平板的Z軸受到重力,在X軸跟Y軸方向不會受到任何重力,所以Z值就是9.8 m/s^2, X值跟Y值就是0 m/s^2,
所以如果從平板螢幕的四個角落用手指捏著,讓平板自然垂落,這時X軸跟Y軸自然受到重力在這兩個方向的分量, X跟Y的值都小於9.8 m/s^2, Z軸因為沒受到任何重力,所以Z值為0 m/s^2.

這邊再附上寫程式去測平板上有哪些sensor的實測畫面:

所有程式碼下載連結:
https://drive.google.com/file/d/0B6EVEd5P2B9cekJiWlZEblBqc2M/view?usp=sharing

2014年9月4日 星期四

RFID project第二彈_RTC時鐘(下) Tiny RTC DS1307 with MSP430G2553

前言:
本篇是RFID project第二彈的下集, 主要介紹的是RTC時鐘 DS1307, 若有不周,歡迎批評指教,缺漏的部份日後會慢慢補上. 轉貼或引用本篇blog內文章段落, 不必交代出處.不過程式碼的部分是拿TI官網找到的usci i2c master Library sample code並參考某網站EEPROM sample code的HW uart部分而寫成的,所以相關著作權法律的問題請注意一下!

過程:
其實一開始拿到的是DS1307,還沒coding之前先上網調查一下這塊板子如何使用,結果發現Vcc是接5V,更有網友提問使用此板跟msp430 i2c溝通有問題,參考某網友的改板電路圖後, 決定要將SDA,SCL的pull-up(3.55V)灌電壓的地方跟DS1307 pin8(Vcc:灌5V)斷開來,因為鈕扣電池座擋到太多trace就算對照schematics拿電表量還是出錯,割錯地方,改板圖請參考圖1. 板子改好之後,
開始coding, 一直不成功, 開始懷疑到板子身上,有可能晶振不振(R7沒焊),也有可能ic壞了, 也有可能pull-up電阻不夠大(一般用4.7k,板上才3.3k), 總之很想去買USB示波器來一探究竟!

082214重大突破:
晚上拿昨天試成功的pcf8563 code來改給ds1307用,結果竟然成功了?!但是更詭異的是ds1307的Vcc灌3.55V(out of spec.)也能工作?!只是有的時候會掉秒而已(大概1分鐘掉個2~3秒), 後來發現Vcc接5V也是掉秒!所以更本不用改板子??? 上網查了一下,關於Vbat跟Vcc接多少電壓的原則如下,只要遵守一定能work:
a.SDA,SCL pull-up voltage must be 3.55Volt(msp430 mcu vcc),
b.ds1307 Vcc > 1.25*Vbat, even Vcc out of spec.(4.5V~5.5V),

1.msp430g2553板子上面的J5 P1.6 jumper拿掉, P1.6當SCL,P1.7當SDA, J3調成HW UART,
2.兩塊板子的Vcc,GND,SDA,SCL互相接在一起,
3.程式用CCSv6編譯, rtc先將目前時間日期寫值進去, 再讀出來, 透過uart秀到PC螢幕上,也就是CCSv6內建陽春版的超級終端機, 最後用while-loop 每秒秀一次時間日期(秒:分:時 星期幾 日-月-年),master Library裡面的TI_USCI_I2C_slave_present();拿掉UCTXSTP,因為跟TX_ISR重複!
->083114更新:發現要改start-up delay需加debug msg(uart印0x0D),
090214更新:上電後只需按msp430板子上的reset按鈕就可達到start-up delay幫助ds1307通電後需要一段時間來ready,
改板圖:


接線圖:



成果圖:



所有程式碼下載連結:
https://drive.google.com/file/d/0B6EVEd5P2B9cRkNOX3l1dk5XZDg/edit?usp=sharing

090814更新:
程式新增DS1307這塊板子上的一顆EEPROM(AT24C32 4KB)的read and write功能,注意write完之後要馬上讀資料的話要delay一段時間,才能接著讀值,

所有程式碼下載連結:
https://drive.google.com/file/d/0B6EVEd5P2B9cX2k4c3RJb29PcGc/edit?usp=sharing

2014年8月21日 星期四

RFID project第二彈_RTC時鐘(上) NXP PCF8563 with MSP430G2553

前言:
繼上回gameduino牛刀小試後,這篇blog要介紹的是RTC時鐘 PCF8563, 若有不周,歡迎批評指教,缺漏的部份日後會慢慢補上. 轉貼或引用本篇blog內文章段落, 不必交代出處.不過程式碼的部分是拿TI官網找到的usci i2c master Library sample code並參考某網站EEPROM sample code的HW uart部分而寫成的,所以相關著作權法律的問題請注意一下!

過程:
起先板子買錯買成5V tiny rtc ds1307 for arduino,目前已改過板子改成可以3V用的,但是coding還是沒成功, 軟體方面一直受挫,就會懷疑到硬體上, 板子上還有一顆EEPROM 24C32不用改板,3V的msp430g2553也可用I2C跟它溝通, coding ok就證明msp430g2553在usci i2c介面是ok的, 衝動之下又去買了3V專用的rtc nxp pcf8563, 起先用EEPROM coding的方式不成功, 又改用usci i2c master Library coding方式也不成功, 想要用傳統的8051隨便找兩根GPIO pin來用軟體模擬硬體I2C的SDA,SCL訊號, 更差點衝動去買USBee示波器(邏輯分析儀), 有稍微check一下是否是硬體有問題(pull-up電阻雖只有1k,但是查datasheet Rpu公式後發現1k剛好,晶振也接地,晶振電路也ok,程式看的出來硬體並非完全都有問題). 因出差所以暫時放著兩天後, 看書提到msp430g2553 usci i2c 7-bits slave address的值就是看datasheet上面提到的slave address 7bits + R/W bit=1 byte, 整個 byte 右移1位就是msp430g2553在用的slave address, 先前被datasheet提到write就用0xA2, read就用0xA3給蒙蔽, 其實答案是:0xA2(或0xA3)>>1,果然改了之後就成功了!

1.msp430g2553板子上面的J5 P1.6 jumper拿掉, P1.6當SCL,P1.7當SDA, J3調成HW UART,
2.兩塊板子的Vcc,GND,SDA,SCL互相接在一起,
3.程式用CCSv6編譯, rtc先將目前時間日期寫值進去, 再讀出來, 透過uart秀到PC螢幕上,也就是CCSv6內建陽春版的超級終端機, 最後用while-loop 每秒秀一次時間日期(秒:分:時 星期幾 日-月-年),master Library裡面的TI_USCI_I2C_slave_present();拿掉UCTXSTP,因為跟TX_ISR重複!
->083014更新:加入start-up delay幫助pcf8563通電後需要一段時間來ready,新發現:pcf8563會掉秒!

接線圖:


成果圖:


所有程式碼下載連結:
https://drive.google.com/file/d/0B6EVEd5P2B9cclNvWHFlalNPYUE/edit?usp=sharing

2014年7月20日 星期日

TI Launchpad with Gameduino之牛刀小試

前言:
近日聽到有人提到需要一個rfid讀卡機+PC螢幕(秀一些資訊)+網路功能, 也想來自己做一個, 在此先想到秀螢幕這部分需求的一個solution, 其他功能的solution日後補上, 若有不周,歡迎批評指教,缺漏的部份日後會慢慢補上. 轉貼或引用本篇blog內文章段落, 不必交代出處.不過程式碼的部分是拿TI板子出廠前preload的demo src code並參考外國網站(google搜尋:launchpad gameduino)而做修改的,所以相關著作權法律的問題請注意一下!

過程:
8051想要接大大的電腦螢幕或液晶電視,不想接陽春LCM模組/LED跑馬燈/七段顯示器/7吋平板電腦/7吋觸控螢幕,又不想花大錢買ARM等級的晶片+linux或DVR板子,該怎麼辦?
survey的結果發現比較便宜的3種解法:
1.'gameduino',
2.picaso uVGA-III(功能更多,價錢更貴),
3.google搜尋8051 vga,照網友提供的做法diy(省更多錢但更費功夫),

這邊我採用的是1.'gameduino':
8051我是用TI launchpad(基本款),然後接gameduino,接法如下表:
p.s.下表所列英文字都是印在板子上的英文字)
 launchpad                gameduino
--------------------------------------------
VCC                         3.3V,+5V
GND                         GND(有三處)
P2.0(CS)                   9(CS)
P1.5(CLK)                13(CLK)
P1.6(LED2)_MISO  12(MISO)
P1.7(MOSI)              11(MOSI)

接線圖:
 
 
 

程式修改的部分:
1.原本出廠板子上會插msp430g2553的IC,裡頭已經預先燒錄程式在裡頭,插上PC端的USB孔,
通電後就會red,green兩個led輪流亮,按下P1.3的按鍵後進入"量溫度模式",量出的溫度是攝氏,
改成華氏,

2.preload程式採用SW UART跟gameduino的SPI介面共用一些launchpad系統資源,導致SW UART失效,所以先將此部分code mark掉,改用gameduino來秀溫度在螢幕畫面上.

3.量出的溫度為兩位數,十位數字跟個位數字分別轉成ascii秀在螢幕畫面上!

成果圖:

所有程式碼下載連結:
https://drive.google.com/file/d/0B6EVEd5P2B9cQk1nWmh6R2t3a1E/edit?usp=sharing

8/3/2014 update:
經過修改launchpad preload程式的SMCLK_16MHz,除頻為4後, 再將PC端uart設定由原來的2400bps->9600bps,就可以在成果圖看到正確的結果, 另外一端PC端超級終端機也可以看到正確的溫度顯示!

所有程式碼下載連結:
https://drive.google.com/file/d/0B6EVEd5P2B9cSjBXRmxpQ2FJU00/edit?usp=sharing