HumanBreak/packages/render-assets/src/composer.ts

445 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
IOption,
IRectangle,
MaxRectsPacker,
Rectangle
} from 'maxrects-packer';
import { Texture } from './texture';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureComposer,
SizedCanvasImageSource
} from './types';
import vert from './shader/pack.vert?raw';
import frag from './shader/pack.frag?raw';
import { compileGLWith } from './utils';
import { logger } from '@motajs/common';
import { isNil } from 'lodash-es';
interface IndexMarkedComposedData {
/** 组合数据 */
readonly asset: ITextureComposedData;
/** 组合时最后一个用到的贴图的索引 */
readonly index: number;
}
export interface IGridComposerData {
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly width: number;
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly height: number;
}
export class TextureGridComposer
implements ITextureComposer<IGridComposerData>
{
/**
* 网格组合器,将等大小的贴图组合成图集,要求每个贴图的尺寸一致。
* 组合时按照先从左到右,再从上到下的顺序组合。
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
readonly maxWidth: number,
readonly maxHeight: number
) {}
private nextAsset(
tex: ITexture[],
start: number,
data: IGridComposerData,
rows: number,
cols: number
): IndexMarkedComposedData {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
const count = Math.min(rows * cols, tex.length - start);
const map = new Map<ITexture, IRect>();
let x = 0;
let y = 0;
for (let i = 0; i < count; i++) {
const dx = x * data.width;
const dy = y * data.height;
const texture = tex[i + start];
const renderable = texture.static();
const { x: sx, y: sy, w: sw, h: sh } = renderable.rect;
ctx.drawImage(renderable.source, sx, sy, sw, sh, dx, dy, sw, sh);
map.set(texture, { x: dx, y: dy, w: sw, h: sh });
x++;
if (x === cols) {
y++;
x = 0;
}
}
const texture = new Texture(canvas);
const composed: ITextureComposedData = {
texture,
assetMap: map
};
return { asset: composed, index: start + count };
}
*compose(
input: Iterable<ITexture>,
data: IGridComposerData
): Generator<ITextureComposedData, void> {
const arr = [...input];
const rows = Math.floor(this.maxWidth / data.width);
const cols = Math.floor(this.maxHeight / data.height);
let i = 0;
while (i < arr.length) {
const { asset, index } = this.nextAsset(arr, i, data, rows, cols);
i = index + 1;
yield asset;
}
}
}
export interface IMaxRectsComposerData extends IOption {
/** 贴图之间的间距 */
readonly padding: number;
}
interface MaxRectsRectangle extends IRectangle {
/** 这个矩形对应的贴图对象 */
readonly data: ITexture;
}
export class TextureMaxRectsComposer
implements ITextureComposer<IMaxRectsComposerData>
{
/**
* 使用 Max Rects 算法执行贴图整合,输入数据参考 {@link IMaxRectsComposerData}
* 输出的纹理的图像源将会是不同的画布,注意与 {@link TextureMaxRectsWebGL2Composer} 区分
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.static().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
for (const bin of packer.bins) {
const map = new Map<ITexture, IRect>();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
ctx.imageSmoothingEnabled = false;
bin.rects.forEach(v => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.static();
const { x, y, w, h } = renderable.rect;
const source = renderable.source;
ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height);
});
const texture = new Texture(canvas);
const data: ITextureComposedData = {
texture,
assetMap: map
};
yield data;
}
}
}
interface RectProcessed {
/** 贴图位置映射 */
readonly texMap: Map<ITexture, Readonly<IRect>>;
/** 顶点数组 */
readonly attrib: Float32Array;
}
export class TextureMaxRectsWebGL2Composer
implements ITextureComposer<IMaxRectsComposerData>
{
/** 使用的画布 */
readonly canvas: HTMLCanvasElement;
/** 画布上下文 */
readonly gl: WebGL2RenderingContext;
/** WebGL2 程序 */
readonly program: WebGLProgram;
/** 顶点数组缓冲区 */
readonly vertBuffer: WebGLBuffer;
/** 纹理数组对象 */
readonly texArray: WebGLTexture;
/** 纹理数组对象的位置 */
readonly texArrayPos: WebGLUniformLocation;
/** `a_position` 的索引 */
readonly posPos: number;
/** `a_texCoord` 的索引 */
readonly texPos: number;
/** 本次处理的贴图宽度 */
private opWidth: number = 0;
/** 本次处理的贴图高度 */
private opHeight: number = 0;
/**
* 使用 Max Rects 算法执行贴图整合,使用 WebGL2 执行组合操作。
* 输入数据参考 {@link IMaxRectsComposerData},要求每个贴图的图像源尺寸一致,贴图大小可以不同。
* 注意,本组合器同时只能处理一个组合操作,上一个没执行完的时候再次调用 `compose` 会出现问题。
* 所有输出的内容中,贴图对象的图像源都是同一个画布,因此获取后要么直接使用,要么立刻调用 `toBitmap`,
* 否则在下一次调用 `next` 时,图像源将会被覆盖。
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {
this.canvas = document.createElement('canvas');
this.canvas.width = maxWidth;
this.canvas.height = maxHeight;
this.gl = this.canvas.getContext('webgl2')!;
const program = compileGLWith(this.gl, vert, frag)!;
this.program = program;
// 初始化画布数据
const texture = this.gl.createTexture();
this.texArray = texture;
const location = this.gl.getUniformLocation(program, 'u_textArray')!;
this.texArrayPos = location;
this.posPos = this.gl.getAttribLocation(program, 'a_position');
this.texPos = this.gl.getAttribLocation(program, 'a_texCoord');
this.vertBuffer = this.gl.createBuffer();
this.gl.useProgram(program);
}
/**
* 对贴图进行索引
* @param textures 贴图数组
*/
private mapTextures(
textures: ITexture[]
): Map<SizedCanvasImageSource, number> {
const map = new Map<SizedCanvasImageSource, number>();
const { width, height } = textures[0].source;
textures.forEach(v => {
const source = v.source;
if (map.has(source)) return;
if (source.width !== width || source.height !== height) {
logger.warn(73);
return;
}
map.set(source, map.size);
});
this.opWidth = width;
this.opHeight = height;
return map;
}
/**
* 传递贴图
* @param texMap 纹理映射
*/
private setTexture(texMap: Map<SizedCanvasImageSource, number>) {
const gl = this.gl;
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texArray);
gl.texStorage3D(
gl.TEXTURE_2D_ARRAY,
1,
gl.RGBA8,
this.opWidth,
this.opHeight,
texMap.size
);
texMap.forEach((index, source) => {
gl.texSubImage3D(
gl.TEXTURE_2D_ARRAY,
0,
0,
0,
index,
this.opWidth,
this.opHeight,
1,
gl.RGBA,
gl.UNSIGNED_BYTE,
source
);
});
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MAG_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MIN_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_S,
gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_T,
gl.CLAMP_TO_EDGE
);
}
/**
* 处理矩形数组,生成 WebGL2 顶点数据
* @param rects 要处理的矩形数组
* @param texMap 贴图到贴图数组索引的映射
*/
private processRects(
rects: MaxRectsRectangle[],
texMap: Map<SizedCanvasImageSource, number>
): RectProcessed {
const { width: ow, height: oh } = this.canvas;
const map = new Map<ITexture, IRect>();
const attrib = new Float32Array(rects.length * 5 * 6);
rects.forEach((v, i) => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.static();
const { width: tw, height: th } = v.data.source;
const { x, y, w, h } = renderable.rect;
// 画到目标画布上的位置
const ol = (v.x / ow) * 2 - 1;
const ob = (v.y / oh) * 2 - 1;
const or = ((v.x + v.width) / ow) * 2 - 1;
const ot = ((v.y + v.height) / oh) * 2 - 1;
// 原始贴图位置
const tl = x / tw;
const tt = y / tw;
const tr = (x + w) / tw;
const tb = (y + h) / th;
// 贴图索引
const ti = texMap.get(v.data.source);
if (isNil(ti)) return;
// Benchmark https://www.measurethat.net/Benchmarks/Show/35246/2/different-method-to-write-a-typedarray
// prettier-ignore
const data = [
// x y u v i
ol, -ot, tl, tt, ti, // 左上角
ol, -ob, tl, tb, ti, // 左下角
or, -ot, tr, tt, ti, // 右上角
or, -ot, tr, tt, ti, // 右上角
ol, -ob, tl, tb, ti, // 左下角
or, -ob, tr, tb, ti // 右下角
];
attrib.set(data, i * 30);
});
const data: RectProcessed = {
texMap: map,
attrib
};
return data;
}
/**
* 执行渲染操作
* @param attrib 顶点数组
*/
private renderAtlas(attrib: Float32Array) {
const gl = this.gl;
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, attrib, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(this.posPos, 2, gl.FLOAT, false, 5 * 4, 0);
gl.vertexAttribPointer(this.texPos, 3, gl.FLOAT, false, 5 * 4, 2 * 4);
gl.enableVertexAttribArray(this.posPos);
gl.enableVertexAttribArray(this.texPos);
gl.uniform1i(this.texArrayPos, 0);
gl.drawArrays(gl.TRIANGLES, 0, attrib.length / 5);
}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
this.opWidth = 0;
this.opHeight = 0;
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.static().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
const indexMap = this.mapTextures(arr);
this.setTexture(indexMap);
for (const bin of packer.bins) {
const { texMap, attrib } = this.processRects(bin.rects, indexMap);
this.renderAtlas(attrib);
const texture = new Texture(this.canvas);
const data: ITextureComposedData = {
texture,
assetMap: texMap
};
yield data;
}
this.gl.disableVertexAttribArray(this.posPos);
this.gl.disableVertexAttribArray(this.texPos);
}
}