過火
再度出擊!這次我們要玩得更火一點---把靜幀變動畫。沒錯,將棋盤格動起來!看一下效果:
這是一個經典的無限偏移動畫,在很多2d橫版射擊遊戲中都會採用的技術。如何在Processing中實現,有兩種比較常見的方法。1.使用相機補位式 2.紋理取樣式
1.相機補位式
( gif 取自 https://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的學習是要經過漫長的適應期的,因為其並行的計算方式和我們解決問題的思考方式是不同的。網上有很多學習的資源,我推薦一個:https://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,如果以畫面3*3
的規格繪製,視窗大小為400*400
,那麼一軸向上的總畫素量為 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幫其著色,做一些素材供其他軟體使用再造,何嘗不是一件很時髦的工作流程。今後,筆者還會使用這種工作流做一些其他工作,敬請期待,謝謝。