OpenGL ES 2.0 Shader 調試新思路(二): 做一個可用的原型


OpenGL ES 2.0 Shader 調試新思路(二): 做一個可用的原型

目錄

背景介紹

請參考前文OpenGL ES 2.0 Shader 調試新思路(一): 改變提問方式

優化 ledChar 函數

前文中我們寫了一個可以用來顯示 1~0 10個數字字型的函數, 不過回頭看看, 發現這個函數有些不太好閱讀, 為方便討論, 把該函數的代碼拷貝在下面:

void ledChar(int,float,float,float,float);

// 構造數字
void ledChar(int n, float xa,float xb, float ya, float yb){
    float x = vTexCoord.x;
    float y = vTexCoord.y;
    float x1 = xa; 
    float x2 = xa+xb;
    float y1 = ya;
    float y2 = ya+yb;
    float ox = (x2+x1)/2.0;
    float oy = (y2+y1)/2.0;
    float dx = (x2-x1)/10.0;
    float dy = (y2-y1)/10.0;
    float b = (x2-x1)/20.0;
    int num = n;
    
    // 設定調試區顯示范圍
    if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
        // 設置調試區背景色
        gl_FragColor = vec4(0.2,0.2,0.8,.5);
        // 分別繪制出 LED 形式的數字 1~0 
        if((num==1 && (x > x2-dx)) ||
           (num==2 && ((y > y2-dy) || (x > x2-dx && y > oy-dy/2.0) || (y > oy-dy/2.0 && y < oy+dy/2.0) || (x < x1+dx && y < oy+dy/2.0) || (y < y1+dy))) ||
           (num==3 && ((y > y2-dy) || (x > x2-dx) || (y > oy-dy/2.0 && y < oy+dy/2.0) ||  (y < y1+dy))) ||
           (num==4 && ((x < x1+dx && y > oy-dy/2.0) ||(x > x2-dx) || (y > oy-dy/2.0 && y < oy+dy/2.0))) ||
           (num==5 && ((y > y2-dy) || (x < x1+dx && y > oy-dy/2.0)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx && y <oy-dy/2.0) || (y<y1+dy))) ||
           (num==6 && ((y > y2-dy) || (x < x1+dx)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx && y <oy-dy/2.0) || (y<y1+dy))) ||
           (num==7 && ((y > y2-dy) || (x > x2-dx))) ||
           (num==8 && ((y > y2-dy) || (x < x1+dx)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx) || (y<y1+dy))) ||
           (num==9 && ((y > y2-dy) || (x < x1+dx && y > oy-dy/2.0)||(y > oy-dy/2.0 && y < oy+dy/2.0)|| (x>x2-dx) || (y<y1+dy))) ||
           (num==0 && ((y > y2-dy) || (x < x1+dx) || (x>x2-dx) || (y<y1+dy)))
           )
        {
            gl_FragColor = vec4(0,1,0,.5);
        }
    }
}

代碼完成后, 看着復雜的判斷條件, 參差不齊的格式, 覺得不太好閱讀, 忽然想到另一種實現 LED 字型的算法, 我們上面的算法是計算7段數碼管每段的坐標范圍, 然后繪制, 最多需要繪制5段(數字2,5,6,8,9), 判斷語句寫起來比較長, 其實我們可以反其道而行之, 意思就是說不畫數字筆畫, 改畫數字筆畫間的矩形, 因為仔細分析一下每個數字字型, 就會發現每個數字都可以用1個或2個小矩形分割出來, 上個示意圖, 第一個示意圖是傳統的繪制方式, 第二個示意圖是我們剛剛想到的用"矩形掩碼", 或者叫"蒙版", 總之就這個意思:

傳統LED數字繪制原理圖 VS. 新想到的利用"矩形掩碼"繪制原理圖:

截圖:

為了方便起見, 寫了一個計算矩形區域的輔助函數 inRect, 我們把新寫的函數命名為 ledRectChar, 代碼如下:

float x = vTexCoord.x;
float y = vTexCoord.y;

void ledRectChar(int,float,float,float,float);
bool inRect(float,float,float,float);

bool inRect(float x1,float x2, float y1, float y2){
    if(x>x1 && x<x2 && y>y1 && y<y2) { return true; } else { return false; }
}

void ledRectChar(int n, float xa,float xb, float ya, float yb){
    float x1 = xa; 
    float x2 = xa+xb;
    float y1 = ya;
    float y2 = ya+yb;
    float ox = (x2+x1)/2.0;
    float oy = (y2+y1)/2.0;
    float dx = (x2-x1)/10.0;
    float dy = (y2-y1)/10.0;
    float b = (x2-x1)/20.0;
    int num = n;
    
    // 設定調試區顯示范圍
    if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
        // 設置調試區背景色為綠色
        gl_FragColor = vec4(0.2,1.0,0.2,1.0);
        // 分別繪制出 LED 形式的數字 1~0 , 用黑色繪制1個或2個矩形,由矩形以外的綠色區域組成字型
        if((num==1 && (inRect(x1,ox-dx,y1,y2) || inRect(ox+dx,x2,y1,y2))) ||
           (num==2 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2,y1+dy,oy-dy/2.0))) ||
           (num==3 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==4 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2) || inRect(x1,x2-dx,y1,oy-dy/2.0))) ||
           (num==5 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==6 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy))) ||
           (num==7 && inRect(x1,x2-dx,y1,y2-dy)) ||
           (num==8 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==9 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==0 && inRect(x1+dx,x2-dx,y1+dy,y2-dy))
          )
        {
            gl_FragColor = vec4(0,0,0,.5);
        }       
    }
}

這樣考慮角度一變, 就發現其實用矩形在這種場景下更簡單, 而且代碼看起來清楚多了, 數字 70 用了一個矩形, 其他數字都用兩個矩形就"掩"出來了(其實1用1個矩形就可以, 用兩個是為了更美觀一些)
.

另外, 貌似使用了太多的函數, 導致效率不高, 其實我也很樂意把這些函數全部都寫成宏, 只是不太會寫帶參數的宏, 試了半天 inRect, 比如

// ! 說明, 這是錯的, 編譯不通過
#define inRect(x1,x2,y1,y2) x>(x1)&&x<(x2)&&y>(y1)&&y<(y2)?true:false

結果老是有錯誤, 就沒繼續研究了(后來仔細研究了 OpenGL Shader Language 之后才發現它不允許帶參數的宏).

改進為可用的原型

用了改進版的 ledRectChar 作為基礎函數, 我們開始考慮實際的使用場景, 實際編程過程中 shader 用到的變量的值肯定不會只是一個一位整數, 所以我們首先得考慮多位整數, 其次還要考慮浮點數, 另外還要考慮負數的表示, 最后要考慮的是表示范圍和准確度(這點最麻煩, 本文只是大致說一下).

列一下后續的需求清單:

  • 多位正整數
  • 多位浮點數
  • 負數
  • 給出表示范圍和准確度

接下來我們一項一項來

表示多位正整數

在我發出前文 OpenGL ES 2.0 Shader 調試新思路(一): 改變提問方式 后, 論壇上的一位朋友 @dave1707 用我們的基礎函數 ledChar 寫了一段表示多位正整數的代碼, 並建議我把它完善一下, 首先表示感謝, 他的代碼如下:

highp int nbr=8293;   // number to display

float m=0.96;
while (nbr>0)
{   m=m-0.015;
    int nn=nbr-((nbr/10)*10);
    ledChar(nn, m, 0.01, 0.96, 0.01);
    nbr=nbr/10;
}

也就是說多位正整數的需求已經解決, 這里存在一個存儲精度的問題, 也就是當要表示的數字大於某個值時就會導致溢出, 這種情況我們不做太多處理, 主要是因為這里處理起來比較麻煩, 我們會在注釋中說明本函數適用的數字范圍.

接下來我們會在他的代碼的基礎上繼續前進, 我們先分析一下 多位浮點數負數 這兩個需求, 發現它們一個需要 小數點, 一個需要負號, 也就是說在我們的基礎函數 ledRectChar 中需要新增兩種字型, 那么我們先來升級一下基礎函數.

新增的兩種字型:小數點和負號

先來處理小數點, 前面用變量 num 的值 1~0 分別表示 1~0 這10個數字的字型, 那么新增的小數點和負號分別用數字 1011 表示, 然后用"矩形掩碼"把它們的字型畫出.

增加這么兩行判斷:

(num==10 && (inRect(x1,x2,oy-dy,y2) || inRect(x1,ox-dx*2.0,y1,oy-dy) || inRect(ox+dx*2.0,x2,y1,oy-dy) )) ||
(num==11 && (inRect(x1,x2,oy+dy,y2) || inRect(x1,x2,y1,oy-dy))) 

更新后的 ledRectChar 函數如下:

void ledRectChar(int n, float xa,float xb, float ya, float yb){
    float x1 = xa; 
    float x2 = xa+xb;
    float y1 = ya;
    float y2 = ya+yb;
    float ox = (x2+x1)/2.0;
    float oy = (y2+y1)/2.0;
    float dx = (x2-x1)/10.0;
    float dy = (y2-y1)/10.0;
    float b = (x2-x1)/20.0;
    int num = n;
    
    // 設定調試區顯示范圍
    if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
        // 設置調試區背景色
        gl_FragColor = vec4(0.2,1.0,0.2,1.0);
        // 分別繪制出 LED 形式的數字 1~0 , 用黑色繪制1個或2個矩形,由矩形以外的綠色區域組成字型
        if((num==1 && (inRect(x1,ox-dx,y1,y2) || inRect(ox+dx,x2,y1,y2))) ||
           (num==2 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2,y1+dy,oy-dy/2.0))) ||
           (num==3 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==4 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2) || inRect(x1,x2-dx,y1,oy-dy/2.0))) ||
           (num==5 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==6 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy))) ||
           (num==7 && inRect(x1,x2-dx,y1,y2-dy)) ||
           (num==8 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==9 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==0 && inRect(x1+dx,x2-dx,y1+dy,y2-dy)) ||
           // 傳入10則繪制小數點, 傳入11則繪制負號, 傳入12則清空
           (num==10 && (inRect(x1,x2,oy-dy,y2) || inRect(x1,ox-dx*2.0,y1,oy-dy) || inRect(ox+dx*2.0,x2,y1,oy-dy) )) ||
           (num==11 && (inRect(x1,x2,oy+dy,y2) || inRect(x1,x2,y1,oy-dy))) ||
           (num==12)
          )
        {
            gl_FragColor = vec4(0,0,0,.5);
        }       
    }
}

浮點數和負數

調試通過, OK, 現在基礎函數已經可以提供全部字型了, 我們接着來實現浮點數的表示, 基於前面 @dave1707 的代碼, 把浮點數先取絕對值(為了避免要區分正負數分別對應的floorceil兩種取整方式), 再把絕對值分離出整數部分和小數部分, 然后都當做整數來處理, 按個十百千萬...位插入一個20位的數組, 整數部分和小數部分中間插一個小數點所對應的數字10, 最后判斷一下是不是負數, 是的話就插入負號所對應的數字11, 還有就是開始要初始化一下數組, 初始值置為 12(在基礎函數中對應黑色背景), 否則數組默認值都是 0, 顯示時會在空白的數字位全部顯示為 0, 影響觀感, 代碼如下:

void showFloat(float f){
    int myNum[20];
    int k = 0;
    int iPart = int(floor(abs(f)));
    int fPart = int(fract(abs(f))*100000.0);
    float m=0.86;
    
    // 初始化數組,全部置為代表黑色的12
    for(int i=0; i<20; i++){
        myNum[i] = 12;
    }

    // 插入小數部分
    while (fPart>0)
    {   
        // 從個位開始, 依次取出個位,十位,百位,千位...的數字值
        myNum[k++]=fPart-((fPart/10)*10);
        fPart=fPart/10;
    }
    
    // 如果是0
    if(f==0.0){myNum[k++] = 0;}

    // 插入小數點
    myNum[k++] = 10;
    
    // 插入整數部分
    while (iPart>0)
    {   
        myNum[k++]=iPart-((iPart/10)*10);
        iPart=iPart/10;
    }
    
    // 如果是負數,則插入代表負號的11
    if(f<0.0) { myNum[k++]=11;}
   
    // 循環輸出數字數組
    for(int i=0; i<20; i++)
    {
        m = m-0.03;
        ledRectChar(myNum[i], m, 0.02, 0.6, 0.15);
    }
    
}

很好, 調試通過, 基本搞定, 好像忘記處理負整數了, 為了避免麻煩, 我們可以建議用戶把負整數進行強制類型轉換為負浮點數, 就可以直接使用我們的 showFloat 函數了, 具體來說就是這么調用:

  • showFloat(float(-1234));

顯示截圖如下:

顯示范圍和准確度

最后說一下這個不得不說的問題, 很多編程語言都需要考慮一個數值表示范圍, 尤其是浮點數, 比如 shader 里的浮點數就是數值越大, 小數位越少, 而且這時比較小的小數會被舍掉, 我們為小數部分留了5位, 整數部分留了13位, 當然, 如果你需要調試更大的數, 也可以自己修改數組的大小--不過好像 shader 中太大的數會返回溢出, 大家根據自己的需求看着辦吧.

看看這幾個截圖:

  • showFloat(2097152.411);

  • showFloat(2097152.11);

在此不得不贊嘆一下我大 Common Lisp 的強悍, 畢竟能直接計算 1024^1024 (1024的1024次方)的語言唯有我大 Common Lisp 了, 看看:

截圖:

可用原型的完整代碼

現在我們基本完成一個可用原型了, 雖然效率不怎么樣, FPS甚至降低了20倍(從60降低到3), 但是首先我們解決了有沒有的問題, 好不好的問題就留待后面解決了, 如果有需求那就繼續優化好了, 下面給出可用原型的全部代碼:

shader代碼

myShader = {
vsBase = [[
// vertex shader 代碼
uniform mat4 modelViewProjection;
uniform vec2 uResolution;

attribute vec4 position; 
attribute vec4 color; 
attribute vec2 texCoord;

varying lowp vec4 vColor; 
varying highp vec2 vTexCoord;

void main() {
	vColor=color;
	vTexCoord = texCoord;

	gl_Position = modelViewProjection * position;
}
]],
fsBase = [[
// fragment shader 代碼
precision highp float;
uniform lowp sampler2D texture;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

float x = vTexCoord.x;
float y = vTexCoord.y;

void ledChar(int,float,float,float,float);
void ledRectChar(int,float,float,float,float);
void showInt(int);
void showFloat(float);
bool inRect(float,float,float,float);

void main() {
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;

	// 默認全部設置為黑色
    gl_FragColor = vec4(.1,.1,.1,1);

    showFloat(-.1111111);
    //showFloat(float(-9765));
    
}

void showFloat(float f){
    int myNum[20];
    int k = 0;
    int iPart = int(floor(abs(f)));
    int fPart = int(fract(abs(f))*100000.0);
    float m=0.86;
    
    // 初始化數組,全部置為代表黑色的12
    for(int i=0; i<20; i++){
        myNum[i] = 12;
    }

    // 插入小數部分
    while (fPart>0)
    {   
        // 從個位開始, 依次取出個位,十位,百位,千位...的數字值
        myNum[k++]=fPart-((fPart/10)*10);
        fPart=fPart/10;
    }
    
    // 如果是0
    if(f==0.0){myNum[k++] = 0;}

    // 插入小數點
    myNum[k++] = 10;
    
    // 插入整數部分
    while (iPart>0)
    {   
        myNum[k++]=iPart-((iPart/10)*10);
        iPart=iPart/10;
    }
    
    // 如果是負數,則插入代表負號的11
    if(f<0.0) { myNum[k++]=11;}
   
    // 循環輸出數字數組
    for(int i=0; i<20; i++)
    {
        m = m-0.03;
        ledRectChar(myNum[i], m, 0.02, 0.6, 0.15);
    }    
}

bool inRect(float x1,float x2, float y1, float y2){
    if(x>x1 && x<x2 && y>y1 && y<y2) { return true; } else { return false; }
}

void ledRectChar(int n, float xa,float xb, float ya, float yb){
    float x1 = xa; 
    float x2 = xa+xb;
    float y1 = ya;
    float y2 = ya+yb;
    float ox = (x2+x1)/2.0;
    float oy = (y2+y1)/2.0;
    float dx = (x2-x1)/10.0;
    float dy = (y2-y1)/10.0;
    float b = (x2-x1)/20.0;
    int num = n;
    
    // 設定調試區顯示范圍
    if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
        // 設置調試區背景色
        gl_FragColor = vec4(0.2,1.0,0.2,1.0);
        // 分別繪制出 LED 形式的數字 1~0 , 用黑色繪制1個或2個矩形,由矩形以外的綠色區域組成字型
        if((num==1 && (inRect(x1,ox-dx,y1,y2) || inRect(ox+dx,x2,y1,y2))) ||
           (num==2 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2,y1+dy,oy-dy/2.0))) ||
           (num==3 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==4 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2) || inRect(x1,x2-dx,y1,oy-dy/2.0))) ||
           (num==5 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==6 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy))) ||
           (num==7 && inRect(x1,x2-dx,y1,y2-dy)) ||
           (num==8 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==9 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
           (num==0 && inRect(x1+dx,x2-dx,y1+dy,y2-dy)) ||
           // 傳入10則繪制小數點, 傳入11則繪制負號, 傳入12則清空
           (num==10 && (inRect(x1,x2,oy-dy,y2) || inRect(x1,ox-dx*2.0,y1,oy-dy) || inRect(ox+dx*2.0,x2,y1,oy-dy) )) ||
           (num==11 && (inRect(x1,x2,oy+dy,y2) || inRect(x1,x2,y1,oy-dy))) ||
           (num==12)
          )
        {
            gl_FragColor = vec4(0,0,0,.5);
        }       
    }
}

]]
}

配套Codea代碼

-- Shader debug
displayMode(OVERLAY)
function setup()
    m = mesh()

    m:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)    

    m.shader = shader(myShader.vsBase,myShader.fsBase)
    
    -- m.texture = "Documents:univer"
    m:setColors(color(220,200,200,255))
    
    parameter.watch("m.shader.modelViewProjection")
    parameter.watch("m.shader.uResolution")
    parameter.watch("m.vertices[1]")
end

function draw()
    background(0)
    m:draw()
end

function touched(touch)
    
end

后記

經過一番調試折騰, 終於完成一個剛剛能用的原型, 以后在 Codea 下調試 shader 程序起碼有個工具勉強可用了. 當然, 這幾個函數也可以用於調試其他平台的 shader 程序.

為了提高人類整體的工作效率, 我們后續會把這個原型發布到 Github 上, 以供其他需要觀察 shader 內部變量的同學使用, 起個響亮的名字 ShaderDebugger:

Github 地址:ShaderDebugger


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM