如何实现一个Canvas渲染引擎(八):图片处理

339 阅读16分钟

友情提示

  1. 请先看这里 👉 引言
  2. 源码的GitHub地址 👉 代码
  3. 本文内容对应的代码在feat/image-processing这个分支上,建议切到这个分支查看代码。

1.前言

如果你在使用一个Canvas渲染引擎,那么你一定会使用图片作为画布上的元素,因为很多复杂的图形,是很难通过Graphics类来手动构建的,所以图片处理是一个Canvas渲染引擎必不可少的功能,甚至可以说是最重要的一个功能。

2. BaseTexture和Texture

在WebGL和WebGPU中,都使用了Texture来表示图片,在这个渲染引擎中亦是如此。

2.1 BaseTexture和Texture的区别

在WebGL/WebGPU中,有一个最大Texture数量限制,虽然我们能上传无数张Texture到GPU中,但是在一个drawCall内,我们只能使用固定数量的texture,在目前最新版本的Chrome中,这个固定的数量=16,也就是说,在一个drawCall内我们只能访问16张不同的图片,要是想访问17张不同的图片,我们只能拆分成2个drawCall进行。

为了保证性能最大化,我们希望drawCall的次数越少越好,所以业界存在着一个办法:把多张图片放到一张大图片里,要用哪张图片,就截取这张图片对应的区域,这样就可以在16张Texture限制的情况下尽可能地减少drawDall次数,这张大图片就叫做:雪碧图。这里的雪碧图,也就是我们的BaseTexture,而我们截取的区域,就是Texture,Texture本身并不包含图片的内容,他主要包含的信息就是:我们要截取雪碧图上的哪一块区域?而图片本身的内容全部在BaseTexture里。这就是BaseTexture和Texture的区别。

2.2 BaseTexture的实现

创建BaseTexture,首先需要一个图片资源,图片资源可以是图片,也可以是Canvas元素。

为了让代码更好写一些,图片资源必须是已加载完毕的,否则这个渲染引擎会抛出一个错误。在这里有2种情况,如果图片资源是一个Image,那么我们用complete属性来确定这个图片资源是否加载完毕,如果图片资源是一个Canvas元素,那么我们可以直接确认它加载完毕的。

2.2.1 构造函数

constructor(source: BaseTextureSource) {
  validateSource(source)

  // 设置一些参数,如:图片资源的宽高等
  if (source instanceof HTMLImageElement) {
    this.handleImgLoaded(source)
  } else if (source instanceof HTMLCanvasElement) {
    this.handleCanvasLoaded(source)
  }

  this.source = source
}

2.2.2 from函数

为了避免重复创建,我们可以对创建过程做一个缓存处理

static from(source: BaseTextureSource) {
  validateSource(source)

  const mySource = source as any

  if (!mySource._baseTextureId) {
    mySource._baseTextureId = uniqueId('base-texture')
  }

  const cacheId = mySource._baseTextureId

  if (!baseTextureCache[cacheId]) {
    baseTextureCache[cacheId] = new BaseTexture(source)
  }

  return baseTextureCache[cacheId]
}

2.3 Texture的实现

Texture是BaseTexture的‘视图’,它本身不包含图片资源,它只包含一些代表了如何截取BaseTexture的一些信息。

2.3.1 构造函数

constructor(baseTexture: BaseTexture, crop?: Rectangle) {
  // crop是裁切区域,它是一个矩形,如果不传crop的话,就表示使用整个BaseTexture的区域
  if (crop) {
    if (crop.width === 0 || crop.height === 0) {
      throw new Error(`裁切区域的长宽不能为0`)
    }

    this.crop = crop
  } else {
    this.crop = new Rectangle(0, 0, baseTexture.width, baseTexture.height)
  }

  this.baseTexture = baseTexture
}

2.3.2 from函数

Texture也包含一个from函数,它可以用来快速创建Texture,并且会走缓存。

static from(source: BaseTextureSource) {
  validateSource(source)

  const mySource = source as any

  if (!mySource._textureId) {
    mySource._textureId = uniqueId('texture')
  }

  const cacheId = mySource._textureId

  if (!textureCache[cacheId]) {
    textureCache[cacheId] = new Texture(BaseTexture.from(source))
  }

  return textureCache[cacheId]
}

3. 新的shader的设计

在前面的一系列文章中,我们并没有涉及到图片的处理,所以shader的结构比较地简单,而现在要加入图片处理了,shader的设计以及顶点的设计也将变得更加复杂了。

3.1 处理图片

首先要明确的一点是,虽然我们现在要处理带图片以及不带图片两种情况,但是我们的shader依然只有一套,而不是2套,具体做法是,无论是带图片还是不带图片,我们都视作带图片来处理。

3.1.1 思想

我们会有2个颜色源,一个来自我们手动指定的颜色(颜色源1),一个来自Texture的颜色(颜色源2)。

当我们要用颜色填充时,颜色源2指向一张纯白(rgba为1,1,1,1)的图片;当我们要用Texture填充时,颜色源1为纯白(rgba为1,1,1,1),然后让两个颜色源相乘,就得到了最终的颜色。

通过以上的方式,我们就可以只用一套shader来同时处理带图片以及不带图片两种情况了。

3.1.2 纯白Texture

我们可以用Canvas元素来创建一个纯白的Texture

export const EMPTY = document.createElement('canvas')
EMPTY.width = 16
EMPTY.height = 16
const ctx = EMPTY.getContext('2d') as CanvasRenderingContext2D
ctx.fillStyle = '#ffffffff'
ctx.fillRect(0, 0, 16, 16)

然后用这个Canvas创建一个Texture,挂载在Texture.EMPTY上

static EMPTY = new Texture(BaseTexture.from(EMPTY))

3.2 顶点的结构

在之前,一个顶点会包含:x和y坐标(2个Float32),颜色信息(4个Uint8),一共12个字节。加入了图片处理后,一个顶点包含的内容变得更多了,我们会给顶点添加这些新东西:uv坐标(2个Float32),textureId(一个Uint32),所以,新的顶点结构将包含24个字节

textureId这个属性比较重要,它记录着:该顶点要用16张texture中的哪一张?

3.3着色器代码

3.3.1 顶点着色器(WebGL)

precision highp float;
attribute vec2 a_position;
attribute vec2 a_uv;
attribute vec4 a_color;
attribute float a_texture_id;

varying vec2 v_uv;
varying vec4 v_color;
varying float v_texture_id;

uniform mat3 u_root_transform;
uniform mat3 u_projection_matrix;

void main(){
  v_uv = a_uv;
  v_color = vec4(a_color.rgb * a_color.a, a_color.a);
  v_texture_id = a_texture_id;

  gl_Position = vec4((u_projection_matrix * u_root_transform * vec3(a_position, 1.0)).xy, 0.0, 1.0);
}

3.3.2 片元着色器(WebGL)

precision mediump float;

varying vec2 v_uv;
varying vec4 v_color;
varying float v_texture_id;

uniform sampler2D u_samplers[16];

void main(){
  vec4 color;

  if(v_texture_id < 0.5)
  {
    color = texture2D(u_samplers[0], v_uv);
  }
  else if(v_texture_id < 1.5)
  {
    color = texture2D(u_samplers[1], v_uv);
  }
  else if(v_texture_id < 2.5)
  {
    color = texture2D(u_samplers[2], v_uv);
  }
  else if(v_texture_id < 3.5)
  {
    color = texture2D(u_samplers[3], v_uv);
  }
  else if(v_texture_id < 4.5)
  {
    color = texture2D(u_samplers[4], v_uv);
  }
  else if(v_texture_id < 5.5)
  {
    color = texture2D(u_samplers[5], v_uv);
  }
  else if(v_texture_id < 6.5)
  {
    color = texture2D(u_samplers[6], v_uv);
  }
  else if(v_texture_id < 7.5)
  {
    color = texture2D(u_samplers[7], v_uv);
  }
  else if(v_texture_id < 8.5)
  {
    color = texture2D(u_samplers[8], v_uv);
  }
  else if(v_texture_id < 9.5)
  {
    color = texture2D(u_samplers[9], v_uv);
  }
  else if(v_texture_id < 10.5)
  {
    color = texture2D(u_samplers[10], v_uv);
  }
  else if(v_texture_id < 11.5)
  {
    color = texture2D(u_samplers[11], v_uv);
  }
  else if(v_texture_id < 12.5)
  {
    color = texture2D(u_samplers[12], v_uv);
  }
  else if(v_texture_id < 13.5)
  {
    color = texture2D(u_samplers[13], v_uv);
  }
  else if(v_texture_id < 14.5)
  {
    color = texture2D(u_samplers[14], v_uv);
  }
  else 
  {
    color = texture2D(u_samplers[15], v_uv);
  }
  
  // 两个颜色源相乘就得到了最终的颜色
  gl_FragColor = color * v_color;
}

片元着色器的代码比较长,因为用了一个switch语句来判断要使用16张Texture的哪一张。

3.3.3 顶点着色器(WebGPU)

@group(0) @binding(0) var<uniform> u_root_transform: mat3x3<f32>;
@group(0) @binding(1) var<uniform> u_projection_matrix: mat3x3<f32>;

struct VertOutput {
  @builtin(position) v_position: vec4<f32>,
  @location(0) v_uv: vec2<f32>,
  @location(1) v_color: vec4<f32>,
  @location(2) @interpolate(flat) v_texture_id : u32,
};

@vertex
fn main(
  @location(0) a_position: vec2<f32>,
  @location(1) a_uv: vec2<f32>,
  @location(2) a_color: vec4<f32>,
  @location(3) a_texture_id_etc: vec4<u32>
) -> VertOutput {

  let v_position = vec4<f32>((u_projection_matrix * u_root_transform * vec3<f32>(a_position, 1.0)).xy, 0.0, 1.0);
  let v_uv = a_uv;
  let v_color = vec4<f32>(a_color.rgb * a_color.a, a_color.a);
  let v_texture_id = a_texture_id_etc.x;

  return VertOutput(v_position, v_uv, v_color, v_texture_id);
}

3.3.4 片元着色器(WebGPU)

@group(1) @binding(0) var u_sampler: sampler;
@group(1) @binding(1) var u_base_texture_1: texture_2d<f32>;
@group(1) @binding(2) var u_base_texture_2: texture_2d<f32>;
@group(1) @binding(3) var u_base_texture_3: texture_2d<f32>;
@group(1) @binding(4) var u_base_texture_4: texture_2d<f32>;
@group(1) @binding(5) var u_base_texture_5: texture_2d<f32>;
@group(1) @binding(6) var u_base_texture_6: texture_2d<f32>;
@group(1) @binding(7) var u_base_texture_7: texture_2d<f32>;
@group(1) @binding(8) var u_base_texture_8: texture_2d<f32>;
@group(1) @binding(9) var u_base_texture_9: texture_2d<f32>;
@group(1) @binding(10) var u_base_texture_10: texture_2d<f32>;
@group(1) @binding(11) var u_base_texture_11: texture_2d<f32>;
@group(1) @binding(12) var u_base_texture_12: texture_2d<f32>;
@group(1) @binding(13) var u_base_texture_13: texture_2d<f32>;
@group(1) @binding(14) var u_base_texture_14: texture_2d<f32>;
@group(1) @binding(15) var u_base_texture_15: texture_2d<f32>;
@group(1) @binding(16) var u_base_texture_16: texture_2d<f32>;

@fragment
fn main(
  @location(0) v_uv: vec2<f32>,
  @location(1) v_color: vec4<f32>,
  @location(2) @interpolate(flat) v_texture_id : u32,
) -> @location(0) vec4<f32> {

  var texture_color: vec4<f32>;

  let dx = dpdx(v_uv);
  let dy = dpdy(v_uv);

  switch v_texture_id {
    case 0:{
      texture_color = textureSampleGrad(u_base_texture_1, u_sampler, v_uv, dx, dy);
      break;
    }
    case 1:{
      texture_color = textureSampleGrad(u_base_texture_2, u_sampler, v_uv, dx, dy);
      break;
    }
    case 2:{
      texture_color = textureSampleGrad(u_base_texture_3, u_sampler, v_uv, dx, dy);
      break;
    }
    case 3:{
      texture_color = textureSampleGrad(u_base_texture_4, u_sampler, v_uv, dx, dy);
      break;
    }
    case 4:{
      texture_color = textureSampleGrad(u_base_texture_5, u_sampler, v_uv, dx, dy);
      break;
    }
    case 5:{
      texture_color = textureSampleGrad(u_base_texture_6, u_sampler, v_uv, dx, dy);
      break;
    }
    case 6:{
      texture_color = textureSampleGrad(u_base_texture_7, u_sampler, v_uv, dx, dy);
      break;
    }
    case 7:{
      texture_color = textureSampleGrad(u_base_texture_8, u_sampler, v_uv, dx, dy);
      break;
    }
    case 8:{
      texture_color = textureSampleGrad(u_base_texture_9, u_sampler, v_uv, dx, dy);
      break;
    }
    case 9:{
      texture_color = textureSampleGrad(u_base_texture_10, u_sampler, v_uv, dx, dy);
      break;
    }
    case 10:{
      texture_color = textureSampleGrad(u_base_texture_11, u_sampler, v_uv, dx, dy);
      break;
    }
    case 11:{
      texture_color = textureSampleGrad(u_base_texture_12, u_sampler, v_uv, dx, dy);
      break;
    }
    case 12:{
      texture_color = textureSampleGrad(u_base_texture_13, u_sampler, v_uv, dx, dy);
      break;
    }
    case 13:{
      texture_color = textureSampleGrad(u_base_texture_14, u_sampler, v_uv, dx, dy);
      break;
    }
    case 14:{
      texture_color = textureSampleGrad(u_base_texture_15, u_sampler, v_uv, dx, dy);
      break;
    }
    default:{
      texture_color = textureSampleGrad(u_base_texture_16, u_sampler, v_uv, dx, dy);
      break;
    }
  }

  return texture_color * v_color;
}

总体思想和WebGL是一样的,但是有个细节需要注意,我们使用了textureSampleGrad函数来从Texture中取颜色,而不是用textureSample函数。这是因为,WebGPU要求textureSample函数必须在uniform control flow中使用,这是WebGPU的一个比较蛋疼的限制,具体来说,如果你在条件判断中使用某些函数,那么这个条件判断中的条件,必须是一个uniform变量或者是一些常量,否则你将得到以下错误:

image.png

我们在这里使用了switch语句,这也算是一个条件判断,并且它的条件v_texture_id不是一个uniform变量,而是一个普通的vertex buffer值,所以在这里面我们不能使用textureSample函数,所以转而使用了textureSampleGrad函数。

4.顶点数组的生成

由于引入了图片处理,加之顶点结构也发生了变化,所以顶点数组生成逻辑需要修改。

在这之前,我们是直接将所有顶点数据塞到一个大数组里面,没有做任何额外处理,无论有多少个顶点我们都只会使用一个drawCall。但现在由于有了16张Texture的限制,我们必须要拆分为多个drawCall,每个drawCall都尽可能地包含更多个顶点。

4.1 Batch

4.1.1 Batch的概念

在WebGL/WebGPU模式中,对于任何需要被绘制到画布上的元素,我们都会转换成一系列的batch,比如,在Graphics类中,每个填充或者描边,我们都会视作一个batch,batch包含一系列的顶点以及这一批顶点要用什么颜色。

每个继承自Container类的子类,都需要实现一个buildBatches函数,用来构造batch,以及每个batch类都需要实现packVertices和packIndices函数,用来把batch类实例自身包含的顶点和顶点下标打包到大数组里。

4.1.2 扩充Batch的内容

现在,我们要扩充batch的内容,加入texture属性,表示这个batch要用哪个texture,默认情况下,texture属性为Texture.EMPTY,也就是一张纯白色的图片:

public texture = Texture.EMPTY

4.2 BatchRenderer.packData

在把画布上的所有元素都转换成batch之后,就会调用这个函数把这些batch包含的顶点打包到一个大数组里,并且把大数组拆分成多个drawCall。

这个函数用来构建顶点数组和顶点下标数组以及drawCall,它会遍历batch数组,将这些batch包含的顶点打包到一个大数组里,之前不涉及到图片处理,所以这个函数的逻辑也比较简单,只需要闷头把顶点往大数组里塞就行。

现在我们需要考虑Texture了,在打包顶点的时候,我们会维持一个计数器,在遍历batch数组时,记录目前积攒了哪些texture,每次遇到新texture就让计数器加一,如果计数器超过了16,那么就把当前积攒的顶点和texture记录为一次drawCall,以此类推,我们会得到一系列的drawCall,代码如下:

/**
 * 将数据打包到大数组里并且构建draw call
 */
protected packData() {
  let drawCall = (this.drawCalls[this.drawCallCount] ??= new DrawCall())
  drawCall.texCount = 0

  let lastBatch = this.batches[0]

  for (let i = 0; i < this.batchesCount; i++) {
    const batch = this.batches[i]

    lastBatch = this.batches[i]

    const baseTexture = batch.texture.baseTexture

    if (baseTexture.drawCallTick !== globalTick) {
      // 如果塞满了16张texture,则需要进入到下一个draw call
      if (drawCall.texCount >= MAX_TEXTURES_COUNT) {
        // 首先计算出这次draw call需要绘制哪些顶点
        const lastDrawCall = this.drawCalls[this.drawCallCount - 1]
        const start = lastDrawCall
          ? lastDrawCall.start + lastDrawCall.size
          : 0
        const size = batch.indexStart + batch.indexCount - start
        drawCall.start = start
        drawCall.size = size
        drawCall.updateBindGroupKey()

        // 进入下一个draw call
        this.drawCallCount++
        drawCall = this.drawCalls[this.drawCallCount] ??= new DrawCall()
        drawCall.texCount = 0

        globalTick++
      }

      baseTexture.drawCallTick = globalTick

      drawCall.baseTextures[drawCall.texCount] = baseTexture
      baseTexture.gpuLocation = drawCall.texCount
      drawCall.texCount++
    }

    batch.packVertices(this.vertFloatView, this.vertIntView)
    batch.packIndices(this.indexBuffer)
  }

  if (drawCall.texCount > 0) {
    const lastDrawCall = this.drawCalls[this.drawCallCount - 1]
    const start = lastDrawCall ? lastDrawCall.start + lastDrawCall.size : 0
    const size = lastBatch.indexStart + lastBatch.indexCount - start
    drawCall.start = start
    drawCall.size = size

    // 补充空白
    if (drawCall.texCount < MAX_TEXTURES_COUNT) {
      const diff = MAX_TEXTURES_COUNT - drawCall.texCount
      for (let i = 0; i < diff; i++) {
        drawCall.baseTextures[drawCall.texCount] = Texture.EMPTY.baseTexture
        drawCall.texCount++
      }
    }
    drawCall.updateBindGroupKey()

    this.drawCallCount++
  }

  globalTick++
}

5. Sprite

这是一个新引入的元素类型,它将继承Container类,可以把Sprite类看作DOM中的img标签,它是一个矩形,内容是一张图片,也正是因为它比较简单,我们先从这个类开始。

5.1 构造函数

Sprite类的构造函数要求一个texture作为参数,然后会根据texture的裁切区域来决定uv坐标

constructor(texture: Texture) {
  super()

  this.type = 'sprite'

  this.texture = texture

  this.anchor = new ObservablePoint(this.onAnchorChange)

  this.handleTextureLoaded()
}

5.2 anchor属性

这个属性是pivot属性的便捷属性,它以百分比的形式来设置锚点,但是底层依然是pivot属性实现的。

5.3 width和height属性

如同DOM的img标签一样,如果采用默认的宽高,可能会出现图片过大的情况,所以我们可以通过width和height手动设置图片的宽高,Sprite类的width和height亦是如此。

width和height属性底层是基于scale属性实现的。

set width(value: number) {
  this._width = value
  this.scale.x = this._width / this.naturalWidth
}

5.4 WebGL和WebGPU渲染实现

根据4.1.1的内容,WebGL和WebGPU模式下要实现的内容其实主要就是2部分:如何构造图形的batch以及如何把构造出来的batch打包到大数组里。

5.4.1 构造batch

Sprite本身只有一个batch,这个batch的顶点为固定的4个,也就是矩形的4个顶点,顶点下标为固定的6个,也就是0,1,2,0,2,3。

5.4.2 将batch包含的顶点打包到大数组中(SpriteBatch.packVertices)

for循环固定4次,把顶点坐标、uv坐标、颜色、textureId打包到大数组中

packVertices(floatView: Float32Array, intView: Uint32Array): void {
  const step = BYTES_PER_VERTEX / 4

  const { vertices, uvs, worldTransform } = this.sprite

  const { a, b, c, d, tx, ty } = worldTransform

  // 顶点数量为固定的4个
  for (let i = 0; i < 4; i++) {
    const x = vertices[i * 2] // position.x
    const y = vertices[i * 2 + 1] // position.y
    const u = uvs[i * 2]
    const v = uvs[i * 2 + 1]

    const vertPos = (this.vertexStart + i) * step

    floatView[vertPos] = a * x + c * y + tx
    floatView[vertPos + 1] = b * x + d * y + ty
    floatView[vertPos + 2] = u
    floatView[vertPos + 3] = v
    intView[vertPos + 4] = this.rgba
    intView[vertPos + 5] = this.texture.baseTexture.gpuLocation
  }
}

5.5 Canvas2D渲染实现

使用Canvas2D的drawImage函数来绘制图片(Sprite),代码(Sprite.renderCanvas):

public renderCanvas(renderer: CanvasRenderer): void {
  const ctx = renderer.ctx
  const texture = this.texture

  const source = texture.baseTexture.source

  ctx.globalAlpha = this.worldAlpha

  const { a, b, c, d, tx, ty } = this.worldTransform
  ctx.setTransform(a, b, c, d, tx, ty)

  const crop = texture.crop

  ctx.drawImage(
    source,
    crop.x,
    crop.y,
    crop.width,
    crop.height,
    0,
    0,
    crop.width,
    crop.height
  )
}

6. Graphics类的Texture填充

Graphics类之前只有颜色填充,现在要加入Texture填充。

6.1 beginFill函数

如果要使用颜色填充,则使用这个函数。

这个函数会将当前的Texture设置成Texture.EMPTY,这是一张纯白色图片,它的颜色值乘以我们设置的填充颜色(WebGL/WebGPU模式下),就得到了最终的颜色值。代码如下:

public beginFill(color: string | number = '#000000', alpha = 1) {
  // 在填充参数变化之前,先将已有的path画出来
  this.startPoly()

  if (typeof alpha !== 'number' || alpha < 0 || alpha > 1) {
    throw new Error(`alpha必须是一个0-1之间的数值`)
  }

  this._fillStyle.reset()

  this._fillStyle.color = normalizeColor(color)
  this._fillStyle.alpha = alpha

  if (alpha > 0) {
    this._fillStyle.visible = true
  }

  return this
}

6.2 beginTextureFill函数

如果要使用Texture填充,则使用这个函数。

如果我们要用Texture填充,那么就需要把当前的Texture设置成我们想要的Texture,然后把颜色值设置成纯白色,最后两个颜色值相乘(WebGL/WebGPU模式下),就得到了Texture上的颜色。

代码:

public beginTextureFill(options: { texture: Texture; alpha?: number }) {
  // 在填充参数变化之前,先将已有的path画出来
  this.startPoly()

  const { texture, alpha } = normalizeOption(options)

  this._fillStyle.reset()

  this._fillStyle.alpha = alpha
  this._fillStyle.texture = texture

  if (alpha > 0) {
    this._fillStyle.visible = true
  }

  return this
}

6.3 WebGL和WebGPU渲染实现

根据4.1.1的内容,WebGL和WebGPU模式下要实现的内容其实主要就是2部分:如何构造图形的batch以及如何把构造出来的batch打包到大数组里。

6.3.1 构造batch

每个fill和stroke操作,都会生成一个batch,fillStyle的Texture和strokeStyle的Texture将会作为这个batch的Texture。

6.3.2 将batch包含的顶点打包到大数组中

遍历所有的batch,将顶点和下标以此打包进大数组

packVertices(floatView: Float32Array, intView: Uint32Array): void {
  const step = BYTES_PER_VERTEX / 4

  const vertices = this.graphics.geometry.vertices.data
  const uvs = this.graphics.geometry.uvs.data

  const offset = this.vertexOffset

  const { a, b, c, d, tx, ty } = this.graphics.worldTransform

  for (let i = 0; i < this.vertexCount; i++) {
    const x = vertices[(offset + i) * 2] // position.x
    const y = vertices[(offset + i) * 2 + 1] // position.y
    const u = uvs[(offset + i) * 2]
    const v = uvs[(offset + i) * 2 + 1]

    const vertPos = (this.vertexStart + i) * step

    floatView[vertPos] = a * x + c * y + tx
    floatView[vertPos + 1] = b * x + d * y + ty
    floatView[vertPos + 2] = u
    floatView[vertPos + 3] = v
    intView[vertPos + 4] = this.rgba // color
    intView[vertPos + 5] = this.texture.baseTexture.gpuLocation
  }
}

6.4 Canvas2D渲染实现

对于普通颜色填充,代码跟以前是一样的,但是对于Texture填充,则需要另作处理。

一般情况下,我们都是将一个16进制色值复制给ctx.fillStyle,告诉canvas接下来要用这种颜色填充,但是ctx.fillStyle不仅仅只支持色值,它还支持Texture填充。

我们可以用Canvas的createPattern函数来生成一个填充模式,然后把这个填充模式赋值给fillStyle,这样的话就可以使用Texture填充了。

if (fillStyle.visible) {
  if (fillStyle.texture === Texture.EMPTY) {
    ctx.fillStyle = fillStyle.color
  } else {
    ctx.fillStyle =
      fillStyle.texture.canvasPattern ??
      getCanvasPattern(fillStyle.texture)
  }
}

getCanvasPattern函数代码:

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D

export const getCanvasPattern = (texture: Texture) => {
  const { crop, baseTexture } = texture

  canvas.width = crop.width
  canvas.height = crop.height

  ctx.save()

  ctx.clearRect(0, 0, crop.width, crop.height)

  ctx.drawImage(
    baseTexture.source,
    crop.x,
    crop.y,
    crop.width,
    crop.height,
    0,
    0,
    crop.width,
    crop.height
  )

  ctx.restore()

  texture.canvasPattern = ctx.createPattern(
    canvas,
    'no-repeat'
  ) as CanvasPattern

  return texture.canvasPattern
}

7. draw

经过了前面几节的讲述,我们已经能做到这两件事情:把画布(stage)上的所有元素都构建成batch;把这些batch里的内容塞到大数组里。所以,接下来的事情,就是调用WebGL/WebGPU的绘制API,把这些顶点数据绘制出来。

因为加入了图片处理,所以WebGLRenderer和WebGPURenderer的draw函数也发生了一些变化,如:将顶点数据拆分为多个drawCall;在绘制每个drawCall时需要绑定Texture。

以WebGLRenderer的draw函数为例:

draw(): void {
  const gl = this.gl
  gl.clear(gl.COLOR_BUFFER_BIT)

  // 遍历drawCall数组,绘制所有顶点
  for (let i = 0; i < this.drawCallCount; i++) {
    const { start, size, texCount, baseTextures } = this.drawCalls[i]

    // 绑定Texture
    for (let j = 0; j < texCount; j++) {
      this.textureSystem.bind(baseTextures[j], j)
    }

    // 绘制当前drawCall包含的所有内容
    gl.drawElements(
      gl.TRIANGLES,
      size,
      gl.UNSIGNED_INT,
      start * Uint32Array.BYTES_PER_ELEMENT
    )
  }
}

WebGPURenderer的draw函数的逻辑也是如此。

8. 结语

本文讲述了图片处理这个极其重要的功能,它是最重要的功能之一,也是这个渲染引擎的一个里程碑功能,实现了图片处理后,这个渲染引擎已经实现了大部分核心功能了。如果大家想实现自己想要的功能,也可以往上面补充。

谢谢大家的阅读🙏,如果觉得本文还不错,就点个赞吧👍,作者需要你的鼓励❤️。