阅读 102

WebGL着色器渲染小游戏实战

## 项目起因 经过对 GLSL 的了解,以及 shadertoy 上各种项目的洗礼,现在开发简单交互图形应该不是一个怎么困难的问题了。下面开始来对一些已有业务逻辑的项目做GLSL渲染器替换开发。 起因是看到某些小游戏广告,感觉机制有趣,实现起来应该也不会很复杂,就尝试自己开发一个。 ![https://img10.360buyimg.com/imagetools/jfs/t1/207524/37/4741/35790/6163e375Eb238479c/8ac7051a21d0cdad.jpg](https://img10.360buyimg.com/imagetools/jfs/t1/207524/37/4741/35790/6163e375Eb238479c/8ac7051a21d0cdad.jpg) 游戏十分简单,类似泡泡龙一样的从屏幕下方中间射出不同颜色大小的泡泡,泡泡上浮到顶部,相同颜色的泡泡可以合并成大一级的不同颜色泡泡。简单说就是一个上下反过来的合成大西瓜。 较特别的地方是为了表现泡泡的质感,在颜色相同的泡泡靠近时,会有水滴表面先合并的效果,这一部分就需要用到着色器渲染来实现了。 ## 项目结构 先对逻辑分层 最上层为游戏业务逻辑`Game`,管理游戏开始、结束状态,响应用户输入,记录游戏分数等。 其次为游戏逻辑驱动层`Engine`,管理游戏元素,暴露可由用户控制的动作,引用渲染器控制游戏场景渲染更新。 再往下是物理引擎模块`Physics`,管理游戏元素之间的关系,以及实现`Engine`需要的接口。 与引擎模块并列的是渲染器模块`Renderer`,读取从`Engine`输入的游戏元素,渲染游戏场景。 这样分层的好处是,各个模块可以独立替换/修改;例如在GLSL渲染器开发完成前,可以替换成其他的渲染器,如`2D canvas`渲染器,甚至使用HTML DOM来渲染。 结构图如下: ![https://img14.360buyimg.com/imagetools/jfs/t1/170197/18/21022/8817/61652fddEe3cece96/061103b9d55086fe.png](https://img14.360buyimg.com/imagetools/jfs/t1/170197/18/21022/8817/61652fddEe3cece96/061103b9d55086fe.png) ## 游戏逻辑实现 ### 游戏业务逻辑 Game 因为游戏业务比较简单,这一层只负责做这几件事: 1. 输入HTML canvas元素,指定游戏渲染范围 2. 初始化驱动层`Engine` 3. 监听用户操作事件`touchend/click`,调用`Engine`控制射出泡泡 4. 循环调用`Engine`的`update`更新方法,并检查超过指定高度的泡泡数量,如数量超过0则停止游戏 ```js class Game {  constructor(canvas) {    this.engine = new Engine(canvas)    document.addEventListener('touchend', (e) => {      if(!this.isEnd) {        this.shoot({          x: e.pageX,          y: e.pageY        }, randomLevel())      }    })  }  shoot(pos, newBallLevel) {    // 已准备好的泡泡射出去    this.engine.shoot(pos, START_V)    // 在初始点生成新的泡泡    this.engine.addStillBall(BALL_INFO[newBallLevel])  }  update() {    this.engine.update()    let point = 0;    let overflowCount = 0;    this.engine.physics.getAllBall().forEach(ball => {      if(!ball.isStatic){        point += Math.pow(2, ball.level);        if (ball.position.y > _this.sceneSize.width * 1.2) {          overflowCount++        }      }    })    if(overflowCount > 1){      this.gameEnd(point);    }  }  gameEnd(point) {    this.isEnd = true    ...  } } ``` ### 驱动层 Engine 这一层的逻辑负责管理物理引擎`Physics`和渲染器模块`Renderer`,并暴露交互方法供`Game`调用。 指定了物理引擎模块需提供以下接口方法: 1. 在指定的位置生成固定的泡泡,供用户作下一次操作时使用 2. 把固定的泡泡按指定的方向射出 在更新方法`update`里,读取所有泡泡所在的位置和大小、等级颜色信息,再调用渲染器渲染泡泡。 ```js class Engine {  constructor(canvas) {    this.renderer = new Renderer(canvas)    this.physics = new Physics()  }  addStillBall({ pos, radius, level }) {    this.physics.createBall(pos, radius, level, true)    this.updateRender()  }  shoot(pos, startV) {    this.physics.shoot(pos, startV)  }  updateRender() {    // 更新渲染器渲染信息  }  update() {    // 调用渲染器更新场景渲染    this.renderer.draw()  } } ``` ### 物理引擎模块 Physics 物理引擎使用了`matter.js`,没别的原因,就是因为之前有项目经验,并且自带一个渲染器,可以拿来辅助我们自己渲染的开发。 包括上一节驱动层提到的,物理引擎模块需要实现以下几个功能: 1. 在指定的位置生成固定的泡泡,供用户作下一次操作时使用 2. 把固定的泡泡按指定的方向射出 3. 检查是否有相同颜色的泡泡相撞 4. 相撞的相同颜色泡泡合并为高一级的泡泡 在这之前我们先需要初始化场景: #### 0.场景搭建 左、右、下的边框使用普通的矩形碰撞体实现。 顶部的半圆使用预先画好的`SVG`图形,使用`matter.js`里`SVG`类的`pathToVertices`方法生成碰撞体,插入到场景中。 因为泡泡都是向上漂浮的,所以置重力方向为y轴的负方向。 ```js // class Physics constructor() {  this.matterEngine = Matter.Engine.create()  // 置重力方向为y轴负方向(即为上)  this.matterEngine.world.gravity.y = -1  // 添加三面墙  Matter.World.add(this.matterEngine.world, Matter.Bodies.rectangle(...))  ...  ...  // 添加上方圆顶  const path = document.getElementById('path')  const points = Matter.Svg.pathToVertices(path, 30)  Matter.World.add(this.matterEngine.world, Matter.Bodies.fromVertices(x, y, [points], ...))  Matter.Engine.run(this.matterEngine) } ``` #### 1.在指定的位置生成固定的泡泡,供用户作下一次操作时使用 创建一个圆型碰撞体放到场景的指定位置,并记录为`Physics`的内部属性供射出方法使用。 ```js // class Physics createBall(pos, radius, level, isStatic) {  const ball = Matter.Bodies.circle(pos.x, pos.y, radius, {    ...// 不同等级不同的大小通过scale区分  })  // 如果生成的是固定的泡泡,则记录在属性上供下次射出时使用  if(isStatic) {    this.stillBall = ball  }  Matter.World.add(this.matterEngine.world, [ball]) } ``` #### 2.把固定的泡泡按指定的方向射出 射出的方向由用户的点击位置决定,但射出的速度是固定的。 可以通过点击位置和原始位置连线的向量,作归一化后乘以初速度大小计算。 ```js // class Physics // pos: 点击位置,用于计算射出方向 // startV: 射出初速度 shoot(pos, startV) {  if(this.stillBall) {    // 计算点击位置与原始位置的向量,归一化(使长度为1)之后乘以初始速度大小    let v = Matter.Vector.create(pos.x - this.stillBall.position.x, pos.y - this.stillBall.position.y)    v = Matter.Vector.normalise(v)    v = Vector.mult(v, startV)    // 设置泡泡为可活动的,并把初速度赋予泡泡    Body.setStatic(this.stillBall, false);    Body.setVelocity(this.stillBall, v);  } } ``` #### 3.检查是否有相同颜色的泡泡相撞 其实`matter.js`是有提供两个碰撞体碰撞时触发的`collisionStart`事件的,但是对于碰撞后合并生成的泡泡,即使与相同颜色的泡泡触碰,也不会触发这个事件,所以只能手动去检测两个泡泡是否碰撞。 这里使用的方法是判断两个圆形的中心距离,是否小于等于半径之和,是则判断为碰撞。 ```js // class Physics checkCollision() {  // 拿到活动中的泡泡碰撞体的列表  const bodies = this.getAllBall()  let targetBody, srcBody  // 逐对泡泡碰撞体遍历  for(let i = 0; i < bodies.length; i++) {    const bodyA = bodies[i]    for(let j = i + 1; j < bodies.length; j++) {      const bodyB = bodies[j]      if(bodyA.level === bodyB.level) {        // 用距离的平方比较,避免计算开平方        if(getDistSq(bodyA.position, bodyB.position) <= 4 * bodyA.circleRadius * bodyA.circleRadius) {          // 使用靠上的泡泡作为目标泡泡          if(bodyA.position.y < bodyB.position.y) {            targetBody = bodyA            srcBody = bodyB          } else {            targetBody = bodyB            srcBody = bodyA          }          return {            srcBody,            targetBody          }        }      }    }  }  return false } ``` #### 4.相撞的相同颜色泡泡合并为高一级的泡泡 碰撞的两个泡泡,取y座标靠上的一个作为合并的目标,靠下的一个作为源泡泡,合并后的泡泡座标设在目标泡泡座标上。 源泡泡碰撞设为关闭,并设为固定位置; 只实现合并的功能的话,只需要把源泡泡的位置设为目标泡泡的座标就可以,但为了实现动画过渡,源泡泡的位置移动做了如下的处理: 1. 在每个更新周期计算源泡泡和目标泡泡位置的差值,得到源泡泡需要移动的向量 2. 移动向量的`1/8`,在下一个更新周期重复1、2的操作 3. 当两个泡泡的位置差值小于一个较小的值(这里设为5)时,视为合并完成,销毁源泡泡,并更新目标泡泡的等级信息 ```js // class Physics mergeBall(srcBody, targetBody, callback) {  const dist = Math.sqrt(getDistSq(srcBody.position, targetBody.position))  // 源泡泡位置设为固定的,且不参与碰撞  Matter.Body.setStatic(srcBody, true)  srcBody.collisionFilter.mask = mergeCategory  // 如果两个泡泡合并到距离小于5的时候, 目标泡泡升级为上一级的泡泡  if(dist < 5) {    // 合并后的泡泡的等级    const newLevel = Math.min(targetBody.level + 1, 8)    const scale = BallRadiusMap[newLevel] / BallRaiusMap[targetBody.level]    // 更新目标泡泡信息    Matter.Body.scale(targetBody, scale, scale)    Matter.Body.set(targetBody, {level: newLevel})    Matter.World.remove(this.matterEngine.world, srcBody)    callback()    return  }  // 需要继续播放泡泡靠近动画  const velovity = {    x: targetBody.position.x - srcBody.position.x,    y: targetBody.position.y - srcBody.position.y  };  // 泡泡移动速度先慢后快  velovity.x /= dist / 8;  velovity.y /= dist / 8;  Matter.Body.translate(srcBody, Matter.Vector.create(velovity.x, velovity.y)); } ``` 因为使用了自定义的方法检测泡泡碰撞,我们需要在物理引擎的beforeUpdate事件上绑定检测碰撞和合并泡泡方法的调用 ```js // class Physics constructor() {  ...  Matter.Events.on(this.matterEngine, 'beforeUpdate', e => {    // 检查是否有正在合并的泡泡,没有则检测是否有相同颜色的泡泡碰撞    if(!this.collisionInfo) {      this.collisionInfo = this.checkCollision()    }    if(this.collisionInfo) {      // 若有正在合并的泡泡,(继续)调用合并方法,在合并完成后清空属性      this.mergeBall(this.collisionInfo.srcBody, this.collisionInfo.targetBody, () => {        this.collistionInfo = null      })    }  })  ... } ``` ### 渲染器模块 GLSL渲染器的实现比较复杂,当前可以先使用`matter.js`自带的渲染器调试一下。 在`Physics`模块中,再初始化一个`matter.js`的`render`: ```js class Physics {  constructor(...) {    ...    this.render = Matter.Render.create(...)    Matter.Render.run(this.render)  } } ``` ![https://img10.360buyimg.com/imagetools/jfs/t1/204405/29/10874/61763/61654ca6E47188742/c37609dd575a6226.jpg](https://img10.360buyimg.com/imagetools/jfs/t1/204405/29/10874/61763/61654ca6E47188742/c37609dd575a6226.jpg) ## 开发定制渲染器 接下来改说一下渲染器的实现了。 先说一下这种像是两滴液体靠近,边缘合并的效果是怎么实现的。 ![https://img11.360buyimg.com/imagetools/jfs/t1/197202/29/13115/172757/6166b94aE2e292112/6a82b57b881d22fd.gif](https://img11.360buyimg.com/imagetools/jfs/t1/197202/29/13115/172757/6166b94aE2e292112/6a82b57b881d22fd.gif) 如果我们把眼镜脱下,或焦点放远一点,大概可以看到这样的图像: ![https://img13.360buyimg.com/imagetools/jfs/t1/210369/27/5106/1129110/6166bd24Ee0563103/f004765f406613f6.gif](https://img13.360buyimg.com/imagetools/jfs/t1/210369/27/5106/1129110/6166bd24Ee0563103/f004765f406613f6.gif) 看到这里可能就有人猜到是怎样实现的了。 是的,就是利用两个边缘径向渐变亮度的圆形,在它们的渐变边缘叠加的位置,亮度的相加能达到圆形中心的程度。 然后在这个渐变边缘的图形上加一个阶跃函数滤镜(低于某个值置为0,高于则置1),就可以得出第一张图的效果。 ## 着色器结构 因为泡泡的数量是一直变化的,而片段着色器`fragmentShader`的`for`循环判断条件(如`i < length`)必须是和常量作判断,(即`length`必须是常量)。 所以这里把泡泡座标作为顶点座标传入顶点着色器`vertexShader`,初步渲染泡泡轮廓: ```glsl // 顶点着色器 vertexShader attribute vec2 a_Position; attribute float a_PointSize; void main() {  gl_Position = vec4(a_Position, 0.0, 1.0);  gl_PointSize = a_PointSize; } ``` ```glsl // 片段着色器 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); } ``` ```js // 渲染器 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`属性。 再调用`WebGL`的`drawArray(gl.POINTS)`方法画点,使每个泡泡渲染成一个顶点。 顶点默认渲染成一个方块,所以我们在片段着色器中,取顶点渲染范围的座标(内置属性)`gl_PointCoord`到顶点中心点(`vec2(0.5, 0.5)`)距离画边缘亮度径向渐变的圆。 如下图,我们应该能得到每个泡泡都渲染成灯泡一样的效果: > 注意这里的WebGL上下文需要指定混合像素算法,否则每个顶点的范围会覆盖原有的图像,观感上为每个泡泡带有一个方形的边框 ```js gl.blendFunc(gl.SRC_ALPHA, gl.ONE) gl.enable(gl.BLEND); ``` ![https://img10.360buyimg.com/imagetools/jfs/t1/201478/20/11114/71506/616796daE84dfb561/1db6b270efc375a1.jpg](https://img10.360buyimg.com/imagetools/jfs/t1/201478/20/11114/71506/616796daE84dfb561/1db6b270efc375a1.jpg) 如上文所说的,我们还需要给这个图像加一个阶跃函数滤镜;但我们不能在上面的片段着色器上直接采用阶跃函数处理输出,因为它是对每个顶点独立渲染的,不会带有其他顶点在当前顶点范围内的信息,也就不会有前面说的「亮度相加」的计算可能。 一个思路是将上面着色器的渲染图像作为一个纹理,在另一套着色器上做阶跃函数处理,作最后实际输出。 对于这样的多级处理,`WebGL`建议使用`FrameBuffer`容器,把渲染结果绘制在上面;整个完整的渲染流程如下: > 泡泡绘制 --> frameBuffer --> texture --> 阶跃函数滤镜 --> canvas 使用`frameBuffer`的方法如下: ```js // 创建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`为渲染目标: ```js gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer) ``` 当`frameBuffer`绘制完成,将自动存储到`0`号纹理中,供第二次的着色器渲染使用 ```glsl // 场景顶点着色器 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; } ``` ```glsl // 场景片段着色器 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` ```js // 渲染器 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);  } } ``` ![https://img14.360buyimg.com/imagetools/jfs/t1/198275/28/13029/78431/6167f9fcE5a2849ab/bf5a28e9f8ca43b0.jpg](https://img14.360buyimg.com/imagetools/jfs/t1/198275/28/13029/78431/6167f9fcE5a2849ab/bf5a28e9f8ca43b0.jpg) ## 不同类型的泡泡区别 在上一节中,实现了游戏里不同位置、不同大小的泡泡在画布上的绘制,也实现了泡泡之间粘合的效果,但是所有的泡泡都是一样的颜色,而且不能合并的泡泡之间也有粘合的效果,这不是我们想要的效果; 在这一节,我们把这些不同类型泡泡做出区别。 要区分各种类型的泡泡,可以在第一套着色器中只传入某个类型的泡泡信息,重复绘制出纹理供第二套场景着色器使用。但每次只绘制一个类型的泡泡会增加很多的绘制次数。 其实在上一节的场景着色器中,只使用了红色通道,而绿色、蓝色通道的值和红色是一样的: ```glsl d = smoothstep(0.6, 0.7, mapColor.r); ``` 其实我们可以在`rgb`3个通道中传入不同类型的泡泡数据(alpha通道的值若为0时,rgb通道的值与设定的不一样,所以不能使用),这样在一个绘制过程中可以绘制3个类型的泡泡;泡泡的类型共有8种,需要分3组渲染。我们在第一套着色器绘制泡泡的时候,增加传入绘制组别和泡泡等级的数据。 并在顶点着色器和片段着色器间增加一个`varying`类型数据,指定该泡泡使用哪一个`rgb`通道。 ```glsl // 修改后的顶点着色器 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);    }  } } ``` ```glsl // 修改后的片段着色器 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个通道作阶跃函数处理(顶点着色器不变),同样传入绘制组序号,区别不同类型的泡泡颜色: ```glsl // 修改后的场景片段着色器 #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`绘制流程,会在每次绘制时清空图像,这需要修改这个默认流程: ```js // 设置WebGL每次绘制时不清空图像 var gl = canvas.getContext('webgl', {  preserveDrawingBuffer: true }); ``` ```js class Renderer {  ...  update() {    gl.clear(gl.COLOR_BUFFER_BIT)// 每次绘制时手动清空图像    this.drawPoint()// 绘制泡泡位置、大小    this.drawScene()// 增加阶跃滤镜  } } ``` ![https://img10.360buyimg.com/imagetools/jfs/t1/212782/30/375/69734/61681561Ebc5ea8ca/765af5efab13fb8f.jpg](https://img10.360buyimg.com/imagetools/jfs/t1/212782/30/375/69734/61681561Ebc5ea8ca/765af5efab13fb8f.jpg) 经过以上处理,整个游戏已基本完成,在这以上可以再修改泡泡的样式、添加分数展示等的部分。 完整项目源码可以访问: [https://github.com/wenxiongid/bubble](https://github.com/wenxiongid/bubble)


作者:凹凸实验室
链接:https://juejin.cn/post/7024061415543537694


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐