Processing 網格(棋盤格)無限偏移紋理動畫

過火

再度出擊!這次我們要玩得更火一點—把靜幀變動畫。沒錯,將棋盤格動起來!看一下效果:

這是一個經典的無限偏移動畫,在很多2d橫版射擊遊戲中都會採用的技術。如何在Processing中實現,有兩種比較常見的方法。1.使用相機補位式 2.紋理取樣式
1.相機補位式

( gif 取自 //www.gameres.com/840857.html

簡單地說是使用幾張圖片素材有機整合在一起,通過視口的偏移量來計算是否需要重置相應圖片素材的位置,如果到了邊緣臨界,那麼相關圖片需要重置其位置。這樣的補位方式才保證了視口裡不被穿幫,觀眾看到的是一個連續的形象和世界。這種方式簡單易懂,操控對象直接,方便調控,但是對於目前我們的程式碼是不合適的,因為整個程式碼是按照面向過程的思路去編寫的,沒有對象概念,對其參數不可單獨控制。

2.紋理取樣式
很明顯是使用紋理貼圖的UV資訊作偏移,因為在三維渲染中,材質UV紋理包裹形式是不同種類的,常見的有:Clamp(拉伸)、Repeat(循環重複\平鋪)、Mirror(鏡像)。這些方式的計算過程已經被封裝在OpenGL或是DircectX應用標準中,只需要在應用時定義好相關屬性方可使用,討巧的技術。
這一次先上程式碼:

PShader shader ;

int increW;
int increH;
int WCOUNT = 10;
int HCOUNT = 10;

void drawRect(PGraphics pg, int c, int x, int y, int w, int h) {

  pg.noStroke();
  pg.fill(c);
  pg.rect(x, y, w, h);
}

PGraphics drawOneGraphics()
{
  PGraphics pg = createGraphics(width, height);
  pg.beginDraw();
  int k = 0;
  int c = 0;
  for (int x = 0; x  < width; x += increW)
  {
    for (int y = 0; y < height; y += increH)
    {
      if (k % 2 == 0)
        c = color(255);
      else 
      c = color(0);
      drawRect(pg, c, x, y, increW, increH);
      k++;
    }
    k++;
  }
  
  //pg.stroke(255,0,0);
  //pg.strokeWeight(10);
  //pg.noFill();
  //pg.rect(0,0,width,height);
  pg.endDraw();
  return pg;
}

void settings() {
  size(400, 400,P2D);
}
void setup() {
  textureWrap(REPEAT);
  increW = width/WCOUNT;
  increH = height/HCOUNT;
  
  shader = loadShader("shader.frag");  
  shader.set("resolution", float(width), float(height));
}

void draw() {
  shader.set("time", millis()/1000.);

  PImage chessboard = drawOneGraphics();
  image(chessboard, 0,0);
  filter(shader);

}

可以看到,這一次我再一次將繪製棋盤格過程進行了封裝,變成了drawOneGraphics()這一函數。因為要使用紋理偏移,Processing默認的渲染框架是JAVA2D,不滿足需求,因此要修改為P2D,在size()函數中添加參數P2D。其次是將著色器PShader導進來,我們要使用它作為紋理著色器為影像著色(其實就是紋理的偏移操作)。Processing著色器是需要通過filter()shader()調用的,前者是作為過濾器,即texture shader紋理著色使用,一般做一些後期處理,而後者是傳統的matrial shader材質著色,用來作用於場景中的模型上。所以大家以後看Processing opengl渲染的程式碼,經常會看到PShader被用在 filter()shader()函數中,留意好不同用法。紋理著色是要放在filter()中的,一般這樣的調用順序:

image(mygraphics1,0,0,);
image(mygraphics2,0,0,);
filter(myshader);        //注意是放在影像繪製好之後調用,作為後期處理,當然也可以理解為 為我們視口大小的面片上著色

而材質著色是要放在shader()中的,調用順序如下:

shader(myshader);
image(mygraphics1,0,0,);

shader(myshader2)
drawMyGeometry();         //注意是放在繪製模型之前調用,作為材質著色器使用

當然有時候你的shader都可以放在任意一個函數中使用,只要調用順序不要搞錯。接下來看看這回的shader源碼:

#ifdef GL_ES                    //這一部分是基於平台的數值精度定義,方便優化
precision mediump float;
precision mediump int;
#endif

#define PROCESSING_TEXTURE_SHADER   //宏定義為Processing 紋理shader,不寫無妨

uniform sampler2D texture;          //等待pde中的默認graphics傳入,Processing底層封裝好的變數名,不能更改,如果更改名字就意味用戶自己的貼圖變數

uniform float time;                 //等待被傳入的時間變數
uniform vec2 resolution;            //視口大小
varying vec4 vertColor;
varying vec4 vertTexCoord;

void main(void) {
  vec2 p = vec2(time*0.1,time*0.1) + gl_FragCoord.xy / resolution.xy ; //待取樣的目標紋理坐標
  vec3 col = texture2D(texture,  p).xyz;//紋理取樣

  vec4 cc = vec4( col, 1.0) ;
  gl_FragColor = cc;                    //輸出片元顏色

}

有些說明我放在了源碼注釋中了。其實glsl的學習是要經過漫長的適應期的,因為其並行的計算方式和我們解決問題的思考方式是不同的。網上有很多學習的資源,我推薦一個://thebookofshaders.com/ 這個網站專門供同學學習shader,並且提供了很多實時編輯預覽的工具,很酷~~~在我們的例子中是使用texture shader。要提的是texture2D()這個函數,2維紋理取樣,將紋理影像通過相應坐標取值,取每一像素的顏色,返回給我們視口中的像素值,也就是gl_FragColor,那如何知道哪個紋理上的值是對應螢幕上的哪個點呢,使用gl_FragCoord.xy / resolution.xy來計算UV,gl_FragCoord表示當前片元著色器處理的候選片元窗口相對坐標資訊,resolution是我們的視窗大小二維向量資訊,一般的紋理著色filter,UV是標準的[0,1]相對坐標值。如果要偏移紋理影像,那麼就得在texture2D()第二個參數上下功夫,將其vec2向量偏移一個值。vec2 p = vec2(time*0.1,time*0.1) + gl_FragCoord.xy / resolution.xy ;在這裡,偏移了vec2(time*0.1,time*0.1)的向量值,把標準UV值和它相加,這樣,最終的效果會是紋理影像在視口中朝著斜45度角偏移。當然讀者可以嘗試不同的角度和速度。
注意到沒有,有些shader屬性是需要外部傳進去的,如resolution、time。在pde中需要使用PShader的set()函數進行傳參。resolution就是視口大小,time,讓其形成動畫的因子,不斷地提高偏移向量值,因為Processing環境的millis()是毫秒級計時,需要除以1000來保證shader中時間概念的一致性。如果讀者還有問題,請留言。

回火

是不是還能再改改,做成不同效果呢。把棋盤格簡化成一色的,然後各自之間留些空隙會比較好看。先繪製靜幀:

其實這裡是有細節要提的,因為留了空隙所以務必計算好留多大,況且要做紋理偏移,大小如果一樣合不合情。答案是否定的。如下圖是正確的做法:

原因是紋理偏移,上一張的縫隙大小會被後一張所承接,因此邊界處的縫隙量保持0.5個單位才能和裡頭的1.0相一致。在計算過程中可以假設邊緣處的距離0.5y,非邊緣處的空隙為y,每方塊大小x,如果以畫面33的規格繪製,窗口大小為400400,那麼一軸向上的總像素量為 X = 3*x + 2*y + 0.5*y*2.經計算,得到400 = 3*x + 3*y ,其中x就是increX,那麼就很容易得出 空隙 y = 視窗寬度 / 方塊個數 - 步長increment。化成程式碼如下:

int edageweight = 10;
int WCOUNT = 8;
int HCOUNT = 8;
int increW;
int increH;

  int k = 0;
  int i, j;
  i = -(increW+edageweight);        //變數i輔助計算繪製起始點
  j = -(increH+edageweight);        //變數j輔助計算繪製起始點
  for (int x=0; x < 10; x++) {
    i += increW+edageweight;

    for (int y=0; y < 10; y++) {
      j += increH+edageweight;

      if (k % 2 == 0)
      {
        int c = color(200, 20, 20);
        drawRect(c, i+edageweight/2, j+edageweight/2, increW, increH);
      }
      k++;
    }
    j = -(increH+edageweight);
    k++;
  }
  i = -(increW+edageweight);

如果用上之前的紋理著色器,那麼會有下面的效果:

pde程式碼:

int WCOUNT = 8;
int HCOUNT = 8;
int increW;
int increH;
int edageweight = 10;
PShader shader ;

void settings() {
  size(400,400, P2D);

}

void setup() {
  textureWrap(REPEAT);

  shader = loadShader("shader.frag");  
  shader.set("resolution", float(width), float(height));
  shader.set("time", millis()/1000.);
  increW = (width)/WCOUNT-edageweight;
  increH = (height)/HCOUNT-edageweight;
}

void draw() {

  shader.set("time", millis()/1000.);

  background(230);
  Process();
  filter(shader);
}

void drawRect(int c, int x, int y, int w, int h) {
  noStroke();
  fill(c);
  rect(x, y, w, h);
}

void Process()
{
  int k = 0;
  int i, j;
  i = -(increW+edageweight);
  j = -(increH+edageweight);
  for (int x=0; x < 10; x++) {
    i += increW+edageweight;

    for (int y=0; y < 10; y++) {
      j += increH+edageweight;

      if (k % 2 == 0)
      {
        int c = color(200, 20, 20);
        drawRect(c, i+edageweight/2, j+edageweight/2, increW, increH);
      }
      k++;
    }
    j = -(increH+edageweight);
    k++;
  }
  i = -(increW+edageweight);
}

還想加上滑鼠交互?可以啊,加一個mouse 二維向量吧,shader程式碼如下:

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

#define PROCESSING_TEXTURE_SHADER

uniform sampler2D texture;

uniform float time;
uniform vec2 resolution;
uniform vec2 mouse;
varying vec4 vertColor;
varying vec4 vertTexCoord;

void main(void) {
  vec2 p =  vec2(1,-1)*mouse.xy/resolution.xy  + gl_FragCoord.xy / resolution.xy ;

  vec3 col = texture2D(texture,  p).xyz;
  vec4 cc = vec4( col, 1.0) ;
  gl_FragColor = cc;
}

pde中加入:

shader.set("mouse", (float)mouseX, (float)mouseY);

尾聲

是時候做個總結了,使用Processing繪製一些基礎紋理,然後用上shader幫其著色,做一些素材供其他軟體使用再造,何嘗不是一件很時髦的工作流程。今後,筆者還會使用這種工作流做一些其他工作,敬請期待,謝謝。