程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> VirtuaNES.v0.97源碼探究<2> 圖形查看器

VirtuaNES.v0.97源碼探究<2> 圖形查看器

編輯:關於C語言

啟動NES模擬器,打開我們經典的超級馬裡奧1。

選擇工具->查看器->圖形查看器。會出現如下的一個窗口。

141431758.png

在該窗口上單擊,畫面還會改變。

這些畫面有什麼意義,VirtiaNES模擬器是如何顯示出這些畫面的?

以上幾個問題就是這篇博文的主題了。


響應函數

菜單選項 “圖形查看器” 的響應函數是:

WNDCMD CMainFrame::OnViewCommand( WNDCMDPARAM )

所在文件 Source Files\MainFrame.cpp

VS系列IDE的查找功能應該算是比較強大的,函數具體在哪一行我就不講了。以下是該響應函數的代碼:

WNDCMD  CMainFrame::OnViewCommand( WNDCMDPARAM )
{
    if( !Emu.IsRunning() || !Nes )
        return;
    switch( uID ) {
        case    ID_VIEW_PATTERN:
            if( !m_PatternView.m_hWnd ) {
                m_PatternView.Create( HWND_DESKTOP );
            }
            ::SetWindowPos( m_PatternView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE );
            break;
        case    ID_VIEW_NAMETABLE:
            if( !m_NameTableView.m_hWnd ) {
                m_NameTableView.Create( HWND_DESKTOP );
            }
            ::SetWindowPos( m_NameTableView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE );
            break;
        case    ID_VIEW_PALETTE:
            if( !m_PaletteView.m_hWnd ) {
                m_PaletteView.Create( HWND_DESKTOP );
            }
            ::SetWindowPos( m_PaletteView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE );
            break;
        case    ID_VIEW_MEMORY:
            if( !m_MemoryView.m_hWnd ) {
                m_MemoryView.Create( HWND_DESKTOP );
            }
            ::SetWindowPos( m_MemoryView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE );
            break;
        default:
            break;
    }
}

第1行,函數的參數WNDCMDPARAM是個宏定義,其實是HWND hWnd, UINT uID

hWnd是父窗口也就是主界面的句柄。uID是菜單項的ID。

13-18,其余行的代碼暫且不管。由於我們選擇的是圖像查看器,所以就來到了這個分支。如果是第一 次執行這段代碼,m_PatternView中的窗口是還未創建的。

m_PatternView中的窗口創建成功之後,將窗口顯示出來,這個函數的任務就結束了。


CPatternView

所在文件 Source Files/PatternView.cpp Header Files/PatternView.h

上節中的 m_PatternView 就是類CPatternView的一個對象。


在CPatternView中,Create函數創建窗口,初始化數據。由於創建了窗口,所以響應函數OnCreate被觸發。OnCreate裡會開啟一個定時器,進而又觸發響應函數OnTimer。

好了,OnTimer才是這個類的重點。OnTimer會不斷讀取最新數據,並顯示在窗口中。


位圖信息頭的初始化如下(Create函數中)

m_BitmapHdr.bih.biSize        = sizeof(BITMAPINFOHEADER);
m_BitmapHdr.bih.biWidth       = 128;
m_BitmapHdr.bih.biHeight      = -256;
m_BitmapHdr.bih.biPlanes      = 1;
m_BitmapHdr.bih.biBitCount    = 8;
m_BitmapHdr.bih.biCompression = BI_RGB;
m_BitmapHdr.bih.biClrUsed     = 16;


從中可以看到顯示在圖形查看器中的位圖 寬128個像素,高256個像素,用8位表示一種顏色也就是256色圖),實際使用了16種顏色也就是調色板有16種顏色)。


再繼續之前,先插入些和NES有關的小知識。NES文件中有兩個調色盤鏡像什麼的暫時不考慮),分別是背景調色盤和精靈調色盤。兩個調色盤各占用16字節的大小,每個字節是一個索引,代表了256色中的一色。


好了繼續。Create函數快結束的地方,有一句代碼。

DirectDraw.GetPaletteData( m_Palette );


m_Palette是一個字節數組, 大小為256 * 4個字節,正好能表示256種顏色。再加上這個變量的命名,我猜測這句代碼的功能是將被索引的256個顏色保存在m_Palette中。


差不多該研究一下OnTimer函數了。函數代碼一起貼出來比較亂,所以分開來貼)

LPBYTE  pPAL = (m_SelectPal<4)?&BGPAL[m_SelectPal*4]:&SPPAL[(m_SelectPal&3)*4];
    m_BitmapHdr.rgb[0] = m_Palette[pPAL[0]];
    m_BitmapHdr.rgb[1] = m_Palette[pPAL[1]];
    m_BitmapHdr.rgb[2] = m_Palette[pPAL[2]];
    m_BitmapHdr.rgb[3] = m_Palette[pPAL[3]];

m_SelectPal是一個0-7的整數,鼠標左鍵點擊後會加1。這也就是為什麼點擊圖形查看器,上面的畫面會改變。

1-2行代碼的作用就是依次從BGPAL背景調色板)或SPPAL精靈調色板)中取出4個顏色索引值。

3-6行代碼可以看出圖形查看器的每一幅畫面其實只有4種顏色。


m_lpPattern是CPatternView的成員變量。保存的是待顯示位圖的像素數據。以下代碼是m_lpPattern的賦值:

for( INT i = 0; i < 8; i++ ) {
        if( m_lpBank[i] != PPU_MEM_BANK[i] || PPU_MEM_TYPE[i] == BANKTYPE_CRAM ) {
            m_lpBank[i] = PPU_MEM_BANK[i];
            LPBYTE  lpPtn = PPU_MEM_BANK[i];
            for( INT p = 0; p < 64; p++ ) {
                LPBYTE  lpScn = &m_lpPattern[i*32*128+(p&15)*8+(p/16)*8*128];
                for( INT y = 0; y < 8; y++ ) {
                    BYTE    chr_l = lpPtn[y];
                    BYTE    chr_h = lpPtn[y+8];
                    lpScn[0] = ((chr_h>>6)&2)|((chr_l>>7)&1);
                    lpScn[4] = ((chr_h>>2)&2)|((chr_l>>3)&1);
                    lpScn[1] = ((chr_h>>5)&2)|((chr_l>>6)&1);
                    lpScn[5] = ((chr_h>>1)&2)|((chr_l>>2)&1);
                    lpScn[2] = ((chr_h>>4)&2)|((chr_l>>5)&1);
                    lpScn[6] = ((chr_h>>0)&2)|((chr_l>>1)&1);
                    lpScn[3] = ((chr_h>>3)&2)|((chr_l>>4)&1);
                    lpScn[7] = ((chr_h<<1)&2)|((chr_l>>0)&1);
                    // Next line
                    lpScn+=128;
                }
                // Next pattern
                lpPtn+=16;
            }
        }
    }


PPU_MEM_BANK是一個長度為12的指針數組。前8個指針指向圖案表,後4個指向命名表或屬性表。每個指針指向的空間大小為1K。

想深入了解圖案表、命名表或和屬性表的話,可以下載下面這個文檔,看圖形處理器一章。

http://down.51cto.com/data/951473



下面先講講我對於圖案表、命名表和屬性表的理解。


NES的游戲畫面其實是由32*30個Tile組成的,每個Tile有8*8個像素。NES的畫面只有16種顏色,因此只需要4位就可以表示一個像素。

那麼每個Tile中8*8的像素是如何求得的呢?


命名表中保存了Tile的編號,Tile儲存在圖案表裡。一張圖案表裡有256個Tile,因此尋址一個Tile要一個字節。一張命名表的大小可以算出來了,1字節*32*30=960字節。


圖案表中真正保存的是Tile中的像素的低2位。上面還提到1個圖案表保存了256個Tile,1個Tile有8*8個像素。那麼一個圖案表的大小是2位*256*8*8=32768位=4096字節=4kb

圖案表儲存一個Tile用16個字節,其中1、9字節用來求Tile的第1行像素,2、10字節用來求第二行像素,依次類推。具體計算方式比如,第1行像素的第一個,高1位是第9字節的最高位,低1位是第1字節的最高位。也就是說後八個字節共64位是64個像素的高位,前八個字節共64位是64個像素的低位。假設第1個字節是01010011,第9個字節是10101111,那麼第一行像素就是2 1 2 1 2 2 3 3。


屬性表比較小。1024字節的空間,命名表占用了960字節,剩下64字節就留給屬性表了,屬性表和命名表是配對出現的。屬性表的一個字節保存的是4*4個Tile高2位的像素。計算一下大小32*32/4/4=64字節。


不得不說,這看似別扭的處理方式,在當時那種硬件資源缺乏的年代,還是挺不錯的。


好了,繼續回到我們的代碼。

這是OnTimer函數最重要的一部分代碼為了方便查看,再貼一次)。

for( INT i = 0; i < 8; i++ ) {
        if( m_lpBank[i] != PPU_MEM_BANK[i] || PPU_MEM_TYPE[i] == BANKTYPE_CRAM ) {
            m_lpBank[i] = PPU_MEM_BANK[i];
            LPBYTE  lpPtn = PPU_MEM_BANK[i];
            for( INT p = 0; p < 64; p++ ) {
                LPBYTE  lpScn = &m_lpPattern[i*32*128+(p&15)*8+(p/16)*8*128];
                for( INT y = 0; y < 8; y++ ) {
                    BYTE    chr_l = lpPtn[y]; 
                    BYTE    chr_h = lpPtn[y+8];
                    lpScn[0] = ((chr_h>>6)&2)|((chr_l>>7)&1);
                    lpScn[4] = ((chr_h>>2)&2)|((chr_l>>3)&1);
                    lpScn[1] = ((chr_h>>5)&2)|((chr_l>>6)&1);
                    lpScn[5] = ((chr_h>>1)&2)|((chr_l>>2)&1);
                    lpScn[2] = ((chr_h>>4)&2)|((chr_l>>5)&1);
                    lpScn[6] = ((chr_h>>0)&2)|((chr_l>>1)&1);
                    lpScn[3] = ((chr_h>>3)&2)|((chr_l>>4)&1);
                    lpScn[7] = ((chr_h<<1)&2)|((chr_l>>0)&1);
                    // Next line
                    lpScn+=128;
                }
                // Next pattern
                lpPtn+=16;
            }
        }
    }


有以上基礎,看這段代碼就簡單了。

代碼裡有3個for循環。每個for循環的循環次數我看了很久才明白。第1行的8是這麼出來的,在NES中有2個圖案表,第1個是背景圖案表,第2個是精靈圖案表。代碼的作者還進一步把一個圖案表分成了4份,為什麼這樣,我也不清楚,至少在我看的資料裡還沒有說把1個圖案表分成4份的,總之4*2,8就出來了。

第2,3行的判斷是什麼意思,我還不明白。在我調試程序的時候,這個判斷基本都是為true。

第5行在讀取圖案表的1/4部分

第6行的循環此處64比較明確,1個圖案表有256個tile,四分之一的圖案表就是64個tile。可以知道這個循環是在畫tile。可以試著把64改成1,重新編譯程序,運行,就可以看到1個tile是什麼樣的了。

第9行的循環次數是8,1次循環是在畫1行像素。有了上文對圖案表的講解,具體1個像素是怎麼求出來的應該已經可以理解了。

有一點我還想說一下,代碼的原作者對12-19行的代碼語句的排序實在是干擾了我很久,我一直以為這樣的排序是有什麼具體含義的,要是寫成下面這樣的話,規律就很明顯了。

lpScn[0] = ((chr_h>>6)&2)|((chr_l>>7)&1);
lpScn[1] = ((chr_h>>5)&2)|((chr_l>>6)&1);
lpScn[2] = ((chr_h>>4)&2)|((chr_l>>5)&1);
lpScn[3] = ((chr_h>>3)&2)|((chr_l>>4)&1);                  
lpScn[4] = ((chr_h>>2)&2)|((chr_l>>3)&1);
lpScn[5] = ((chr_h>>1)&2)|((chr_l>>2)&1);
lpScn[6] = ((chr_h>>0)&2)|((chr_l>>1)&1);
lpScn[7] = ((chr_h<<1)&2)|((chr_l>>0)&1);


第21行的意思再解釋一下。tile是正方形的,畫完一行8個像素之後,畫下一行前要先給lpScn加上圖像的寬度128。

第24行比較簡單,圖案表的1個tile已經讀取完了,tile的大小是16字節,為了讀取下一個tile,就得給圖案表指針lpPtn加上16。



好了圖像查看器模塊的代碼差不多就說到這了,下一節講解卷軸查看器的代碼。卷軸就是背景,算是趁熱打鐵吧,學習了圖案表、命名表、屬性表,看看背景是如何根據這三張表得到的。


本文出自 “三人乘虎” 博客,請務必保留此出處http://darhx.blog.51cto.com/7920146/1300053

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved