友情提示
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变量或者是一些常量,否则你将得到以下错误:
我们在这里使用了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. 结语
本文讲述了图片处理这个极其重要的功能,它是最重要的功能之一,也是这个渲染引擎的一个里程碑功能,实现了图片处理后,这个渲染引擎已经实现了大部分核心功能了。如果大家想实现自己想要的功能,也可以往上面补充。
谢谢大家的阅读🙏,如果觉得本文还不错,就点个赞吧👍,作者需要你的鼓励❤️。