1

WebGl渲染器小游戏实战(下)

 2 years ago
source link: https://jelly.jd.com/article/61681ff2f9d0dc019ed537c7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
WebGl渲染器小游戏实战(下)
上传日期:2021.10.25
经过对GLSL的了解,以及shadertoy上各种项目的洗礼,现在来尝试开发使用GLSL渲染的小游戏 承接上篇的进度,接下来这一篇说一下渲染器的实现

先说一下这种像是两滴液体靠近,边缘合并的效果是怎么实现的

11d36b920f7b79f2.png

如果我们把眼镜脱下,或焦点放远一点,大概可以看到这样的图像:

11d36b920f7b79f2.png

看到这里可能就有人猜到是怎样实现的了。

是的,就是利用两个边缘径向渐变亮度的圆形,在它们的渐变边缘叠加的位置,亮度的相加能达到圆形中心的程度。

然后在这个渐变边缘的图形上加一个阶跃函数滤镜(低于某个值置为0,高于则置1),就可以得出第一张图的效果

着色器结构

因为泡泡的数量是一直变化的,而片段着色器fragmentShaderfor循环判断条件(如i < length)必须是和常量作判断,(即length必须是常量)

所以这里把泡泡座标作为顶点座标传入顶点着色器vertexShader,初步渲染泡泡轮廓

// 顶点着色器 vertexShader
attribute vec2 a_Position;
attribute float a_PointSize;

void main() {
  gl_Position = vec4(a_Position, 0.0, 1.0);
  gl_PointSize = a_PointSize;
}
// 片段着色器 fragmentShader
#ifdef GL_ES
precision mediump float;
#endif

void main() {
  float d = length(gl_PointCoord - vec2(0.5, 0.5));
  float c = smoothstep(0.40, 0.20, d);
  gl_FragColor = vec4(vec3(c), 1.0);
}
// 渲染器 Renderer.js
class GLRenderer {
  ...
  // 更新游戏元素数据
  updateData(posData, sizeData) {
    ...
    this.posData = new Float32Array(posData)
    this.sizeData = new Float32Array(sizeData)
    ...
  }
  // 更新渲染
  draw() {
    ...
    // 每个顶点取2个数
    this.setAttribute(this.program, 'a_Position', this.posData, 2, 'FLOAT')
    // 每个顶点取1个数
    this.setAttribute(this.program, 'a_PointSize', this.sizeData, 1, 'FLOAT')
    ...
  }
}

渲染器的js代码中,把每个点的x,y座标合并成一个一维数组,传到着色器的a_Position属性;把每个点的直径同样组成一个数组,传到着色器的a_PointSize属性

再调用WebGldrawArray(gl.POINTS)方法画点,使每个泡泡渲染成一个顶点

顶点默认渲染成一个方块,所以我们在片段着色器中,取顶点渲染范围的座标(内置属性)gl_PointCoord到顶点中心点(vec2(0.5, 0.5))距离画边缘亮度径向渐变的圆

如下图,我们应该能得到每个泡泡都渲染成灯泡一样的效果

注意这里的WebGl上下文需要指定混合像素算法,否则每个顶点的范围会覆盖原有的图像,观感上为每个泡泡带有一个方形的边框

gl.blendFunc(gl.SRC_ALPHA, gl.ONE)
gl.enable(gl.BLEND);
11d36b920f7b79f2.png

如上文所说的,我们还需要给这个图像加一个阶跃函数滤镜;但我们不能在上面的片段着色器上直接采用阶跃函数处理输出,因为它是对每个顶点独立渲染的,不会带有其他顶点在当前顶点范围内的信息,也就不会有前面说的「亮度相加」的计算可能

一个思路是将上面着色器的渲染图像作为一个纹理,在另一套着色器上做阶跃函数处理,作最后实际输出

对于这样的多级处理,WebGl建议使用FrameBuffer容器,把渲染结果绘制在上面;整个完整的渲染流程如下:

泡泡绘制 --> frameBuffer --> texture --> 阶跃函数滤镜 --> canvas

使用frameBuffer的方法如下:

// 创建frameBuffer
var frameBuffer = gl.createFramebuffer()
// 创建纹理texture
var texture = gl.createTexture()
// 绑定纹理到二维纹理
gl.bindTexture(gl.TEXTURE_2D, texture)
// 设置纹理信息,注意宽度和高度需是2的次方幂,纹理像素来源为空
gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA,
  1024,
  1024,
  0,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  null
)
// 设置纹理缩小滤波器
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
// frameBuffer与纹理绑定
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)

使用以下方法,指定frameBuffer为渲染目标

gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer)

frameBuffer绘制完成,将自动存储到0号纹理中,供第二次的着色器渲染使用

// 场景顶点着色器 SceneVertexShader
attribute vec2 a_Position;
attribute vec2 a_texcoord;
varying vec2 v_texcoord;

void main() {
  gl_Position = vec4(a_Position, 0.0, 1.0);
  v_texcoord = a_texcoord;
}
// 场景片段着色器 SceneFragmentShader
#ifdef GL_ES
precision mediump float;
#endif

varying vec2 v_texcoord;
uniform sampler2D u_sceneMap;

void main() {
  vec4 mapColor = texture2D(u_sceneMap, v_texcoord);
  d = smoothstep(0.6, 0.7, mapColor.r);
  gl_FragColor = vec4(vec3(d), 1.0);
}

场景着色器输入3个参数,分别是

  1. a_Position: 纹理渲染的面的顶点座标,因为这里的纹理是铺满全画布,所以是画布的四个角
  2. a_textcoord: 各个顶点的纹理uv座标,因为纹理大小和渲染大小不一样(纹理大小为1024*1024,渲染大小为画布大小),所以是从(0.0, 0.0)(width / 1024, height / 1024)
  3. u_sceneMap: 纹理序号,用的第一个纹理,传入0
// 渲染器 Renderer.js
class Renderer {
  ...
  drawScene() {
    // 把渲染目标设回画布
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    // 使用渲染场景的程序
    gl.useProgram(sceneProgram);
    // 设置4个顶点座标
    this.setAttribute(this.sceneProgram, "a_Position", new Float32Array([
      -1.0,
      -1.0,

      1.0,
      -1.0,

      -1.0,
      1.0,

      -1.0,
      1.0,

      1.0,
      -1.0,

      1.0,
      1.0
    ]), 2, "FLOAT");
    // 设置顶点座标的纹理uv座标
    setAttribute(sceneProgram, "a_texcoord", new Float32Array([
      0.0,
      0.0,

      canvas.width / MAPSIZE,
      0.0,

      0.0,
      canvas.height / MAPSIZE,

      0.0,
      canvas.height / MAPSIZE,

      canvas.width / MAPSIZE,
      0.0,

      canvas.width / MAPSIZE,
      canvas.height / MAPSIZE
    ]), 2, "FLOAT");
    // 设置使用0号纹理
    this.setUniform1i(this.sceneProgram, 'u_sceneMap', 0);
    // 用画三角形面的方法绘制
    this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
  }
}
11d36b920f7b79f2.png

不同类型的泡泡区别

在上一节中,实现了游戏里不同位置、不同大小的泡泡在画布上的绘制,也实现了泡泡之间粘合的效果,但是所有的泡泡都是一样的颜色,而且不能合并的泡泡之间也有粘合的效果,这不是我们想要的效果;

在这一节,我们把这些不同类型泡泡做出区别

要区分各种类型的泡泡,可以在第一套着色器中只传入某个类型的泡泡信息,重复绘制出纹理供第二套场景着色器使用。但每次只绘制一个类型的泡泡会增加很多的绘制次数。

其实在上一节的场景着色器中,只使用了红色通道,而绿色、蓝色通道的值和红色是一样的:

d = smoothstep(0.6, 0.7, mapColor.r);

其实我们可以在rgb3个通道中传入不同类型的泡泡数据(alpha通道的值若为0时,rgb通道的值与设定的不一样,所以不能使用),这样在一个绘制过程中可以绘制3个类型的泡泡;泡泡的类型共有8种,需要分3组渲染。我们在第一套着色器绘制泡泡的时候,增加传入绘制组别和泡泡等级的数据。

并在顶点着色器和片段着色器间增加一个varying类型数据,指定该泡泡使用哪一个rgb通道

// 修改后的顶点着色器 vertexShader
uniform int group;// 绘制的组序号
attribute vec2 a_Position;
attribute float a_Level;// 泡泡的等级
attribute float a_PointSize;
varying vec4 v_Color;// 片段着色器该使用哪个rgb通道

void main() {
  gl_Position = vec4(a_Position, 0.0, 1.0);
  gl_PointSize = a_PointSize;
  if(group == 0){
    if(a_Level == 1.0){
      v_Color = vec4(1.0, 0.0, 0.0, 1.0);// 使用r通道
    }
    if(a_Level == 2.0){
      v_Color = vec4(0.0, 1.0, 0.0, 1.0);// 使用g通道
    }
    if(a_Level == 3.0){
      v_Color = vec4(0.0, 0.0, 1.0, 1.0);// 使用b通道
    }
  }
  if(group == 1){
    if(a_Level == 4.0){
      v_Color = vec4(1.0, 0.0, 0.0, 1.0);
    }
    if(a_Level == 5.0){
      v_Color = vec4(0.0, 1.0, 0.0, 1.0);
    }
    if(a_Level == 6.0){
      v_Color = vec4(0.0, 0.0, 1.0, 1.0);
    }
  }
  if(group == 2){
    if(a_Level == 7.0){
      v_Color = vec4(1.0, 0.0, 0.0, 1.0);
    }
    if(a_Level == 8.0){
      v_Color = vec4(0.0, 1.0, 0.0, 1.0);
    }
    if(a_Level == 9.0){
      v_Color = vec4(0.0, 0.0, 1.0, 1.0);
    }
  }
}
// 修改后的片段着色器 fragmentShader
#ifdef GL_ES
precision mediump float;
#endif

varying vec4 v_Color;

void main(){
  float d = length(gl_PointCoord - vec2(0.5, 0.5));
  float c = smoothstep(0.40, 0.20, d);
  gl_FragColor = v_Color * c;
}

场景片段着色器分别对3个通道作阶跃函数处理(顶点着色器不变),同样传入绘制组序号,区别不同类型的泡泡颜色:

// 修改后的场景片段着色器
#ifdef GL_ES
precision mediump float;
#endif

varying vec2 v_texcoord;
uniform sampler2D u_sceneMap;
uniform vec2 u_resolution;
uniform int group;

void main(){
  vec4 mapColor = texture2D(u_sceneMap, v_texcoord);
  float d = 0.0;
  vec4 color = vec4(0.0);
  if(group == 0){
    if(mapColor.r > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.r);
      color += vec4(0.86, 0.20, 0.18, 1.0) * d;
    }
    if(mapColor.g > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.g);
      color += vec4(0.80, 0.29, 0.09, 1.0) * d;
    }
    if(mapColor.b > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.b);
      color += vec4(0.71, 0.54, 0.00, 1.0) * d;
    }
  }
  if(group == 1){
    if(mapColor.r > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.r);
      color += vec4(0.52, 0.60, 0.00, 1.0) * d;
    }
    if(mapColor.g > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.g);
      color += vec4(0.16, 0.63, 0.60, 1.0) * d;
    }
    if(mapColor.b > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.b);
      color += vec4(0.15, 0.55, 0.82, 1.0) * d;
    }
  }
  if(group == 2){
    if(mapColor.r > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.r);
      color += vec4(0.42, 0.44, 0.77, 1.0) * d;
    }
    if(mapColor.g > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.g);
      color += vec4(0.83, 0.21, 0.51, 1.0) * d;
    }
    if(mapColor.b > 0.0){
      d = smoothstep(0.6, 0.7, mapColor.b);
      color += vec4(1.0, 1.0, 1.0, 1.0) * d;
    }
  }
  gl_FragColor = color;
}

这里使用了分多次绘制成3个纹理图像,处理后合并成最后的渲染图像,场景着色器绘制了3次,这需要在每次绘制保留上次的绘制结果;而默认的WebGl绘制流程,会在每次绘制时清空图像,这需要修改这个默认流程:

// 设置webgl每次绘制时不清空图像
var gl = canvas.getContext('webgl', {
  preserveDrawingBuffer: true
});
class Renderer {
  ...
  update() {
    gl.clear(gl.COLOR_BUFFER_BIT)// 每次绘制时手动清空图像
    this.drawPoint()// 绘制泡泡位置、大小
    this.drawScene()// 增加阶跃滤镜
  }
}
11d36b920f7b79f2.png

经过以上处理,整个游戏已基本完成,在这以上可以再修改泡泡的样式、添加分数展示等的部分

完整项目源码可以访问: https://github.com/wenxiongid/bubble


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK