Press "Enter" to skip to content

Processing 网格(棋盘格)无限偏移纹理动画

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

过火

 

再度出击!这次我们要玩得更火一点—把静帧变动画。没错,将棋盘格动起来!看一下效果:

这是一个经典的无限偏移动画,在很多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帮其着色,做一些素材供其他软件使用再造,何尝不是一件很时髦的工作流程。

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注