diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e578b3e..732944acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -213,6 +213,7 @@ are doing mtv adjustments during precollision. ### Updates +- Perf improvement to image rendering! with ImageRendererV2! Roughly doubles the performance of image rendering - Perf improvement to retrieving components with `ex.Entity.get()` which widely improves engine performance - Non-breaking parameters that reference `delta` to `elapsedMs` to better communicate intent and units - Perf improvements to `ex.ParticleEmitter` diff --git a/sandbox/tests/drawcalls/index.ts b/sandbox/tests/drawcalls/index.ts index 98592b4b1..93477f6e4 100644 --- a/sandbox/tests/drawcalls/index.ts +++ b/sandbox/tests/drawcalls/index.ts @@ -12,7 +12,10 @@ var loader = new ex.Loader([tex]); var random = new ex.Random(1337); -var items = [{ pos: ex.Vector.Zero, vel: ex.vec(random.integer(50, 100), random.integer(50, 100)) }]; +var items = [ + { pos: ex.Vector.Zero, vel: ex.vec(random.integer(50, 100), random.integer(50, 100)) }, + { pos: ex.Vector.Zero, vel: ex.vec(random.integer(50, 100), random.integer(50, 100)) } +]; var drawCalls$ = document.getElementById('draw-calls'); var drawnItems$ = document.getElementById('drawn-items'); var add$ = document.getElementById('add'); @@ -39,7 +42,7 @@ game.start(loader).then(() => { items[i].vel.y *= -1; } } - game.graphicsContext.drawCircle(ex.vec(200, 200), width / 4, ex.Color.Blue, ex.Color.Black, 2); + // game.graphicsContext.drawCircle(ex.vec(200, 200), width / 4, ex.Color.Blue, ex.Color.Black, 2); }; game.onPostDraw = (_engine, deltaMs) => { diff --git a/src/engine/Flags.ts b/src/engine/Flags.ts index 0d18e57a4..60adbfa50 100644 --- a/src/engine/Flags.ts +++ b/src/engine/Flags.ts @@ -16,6 +16,13 @@ export class Flags { Flags.enable('use-canvas-context'); } + /** + * Force excalibur to use the less optimized image renderer + */ + public static useLegacyImageRenderer() { + Flags.enable('use-legacy-image-renderer'); + } + /** * Freeze all flag modifications making them readonly */ diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index d33f6fb6d..8248a7284 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -35,6 +35,8 @@ import { Material, MaterialOptions } from './material'; import { MaterialRenderer } from './material-renderer/material-renderer'; import { Shader, ShaderOptions } from './shader'; import { GarbageCollector } from '../../GarbageCollector'; +import { ImageRendererV2 } from './image-renderer-v2/image-renderer-v2'; +import { Flags } from '../../Flags'; export const pixelSnapEpsilon = 0.0001; @@ -97,20 +99,11 @@ export interface ExcaliburGraphicsContextWebGLOptions extends ExcaliburGraphicsC export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { private _logger = Logger.getInstance(); private _renderers: Map = new Map(); + public imageRenderer: 'ex.image' | 'ex.image-v2' = Flags.isEnabled('use-legacy-image-renderer') ? 'ex.image' : 'ex.image-v2'; private _isDrawLifecycle = false; public useDrawSorting = true; - private _drawCallPool = new Pool( - () => new DrawCall(), - (instance) => { - instance.priority = 0; - instance.z = 0; - instance.renderer = undefined as any; - instance.args = undefined as any; - return instance; - }, - 4000 - ); + private _drawCallPool = new Pool(() => new DrawCall(), undefined, 4000); private _drawCallIndex = 0; private _drawCalls: DrawCall[] = new Array(4000).fill(null); @@ -311,7 +304,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - + gl.depthMask(false); // Setup builtin renderers this.register( new ImageRenderer({ @@ -324,6 +317,12 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this.register(new CircleRenderer()); this.register(new PointRenderer()); this.register(new LineRenderer()); + this.register( + new ImageRendererV2({ + uvPadding: this.uvPadding, + pixelArtSampler: this.pixelArtSampler + }) + ); this.materialScreenTexture = gl.createTexture(); if (!this.materialScreenTexture) { @@ -398,6 +397,11 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { } public draw(rendererName: TRenderer['type'], ...args: Parameters) { + if (process.env.NODE_ENV === 'development') { + if (args.length > 9) { + throw new Error('Only 10 or less renderer arguments are supported!;'); + } + } if (!this._isDrawLifecycle) { this._logger.warnOnce( `Attempting to draw outside the the drawing lifecycle (preDraw/postDraw) is not supported and is a source of bugs/errors.\n` + @@ -421,7 +425,16 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { drawCall.state.opacity = this._state.current.opacity; drawCall.state.tint = this._state.current.tint; drawCall.state.material = this._state.current.material; - drawCall.args = args; + drawCall.args[0] = args[0]; + drawCall.args[1] = args[1]; + drawCall.args[2] = args[2]; + drawCall.args[3] = args[3]; + drawCall.args[4] = args[4]; + drawCall.args[5] = args[5]; + drawCall.args[6] = args[6]; + drawCall.args[7] = args[7]; + drawCall.args[8] = args[8]; + drawCall.args[9] = args[9]; this._drawCalls[this._drawCallIndex++] = drawCall; } else { // Set the current renderer if not defined @@ -435,7 +448,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { } // If we are still using the same renderer we can add to the current batch - renderer.draw(...args); + renderer.draw(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); this._currentRenderer = renderer; } @@ -523,7 +536,11 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { if (this._state.current.material) { this.draw('ex.material', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight); } else { - this.draw('ex.image', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight); + if (this.imageRenderer === 'ex.image') { + this.draw(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight); + } else { + this.draw(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight); + } } } diff --git a/src/engine/Graphics/Context/draw-call.ts b/src/engine/Graphics/Context/draw-call.ts index cd6f6eddb..829c99f81 100644 --- a/src/engine/Graphics/Context/draw-call.ts +++ b/src/engine/Graphics/Context/draw-call.ts @@ -13,5 +13,5 @@ export class DrawCall { tint: Color.White, material: null }; - public args!: any[]; + public args: any[] = new Array(10); } diff --git a/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.frag.glsl b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.frag.glsl new file mode 100644 index 000000000..01344f432 --- /dev/null +++ b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.frag.glsl @@ -0,0 +1,67 @@ +#version 300 es +precision mediump float; + +// UV coord +in vec2 v_texcoord; + +// Textures in the current draw +uniform sampler2D u_textures[%%count%%]; + +uniform bool u_pixelart; + +in float v_texture_index; + +in float v_opacity; + +in vec4 v_tint; + +// texture resolution +in vec2 v_res; + +in vec2 v_uv_min; +in vec2 v_uv_max; + +out vec4 fragColor; + +// Inigo Quilez pixel art filter https://jorenjoestar.github.io/post/pixel_art_filtering/ +vec2 uv_iq(in vec2 uv, in vec2 texture_size) { + vec2 pixel = uv * texture_size; + + vec2 seam=floor(pixel+.5); + vec2 dudv=fwidth(pixel); + pixel=seam+clamp((pixel-seam)/dudv,-.5,.5); + + return pixel/texture_size; +} + +float lerp(float from, float to, float rel){ + return ((1. - rel) * from) + (rel * to); +} + +float invLerp(float from, float to, float value){ + return (value - from) / (to - from); +} + +float remap(float origFrom, float origTo, float targetFrom, float targetTo, float value){ + float rel = invLerp(origFrom, origTo, value); + return lerp(targetFrom, targetTo, rel); +} + +void main(){ + // In order to support the most efficient sprite batching, we have multiple + // textures loaded into the gpu (usually 8) this picker logic skips over textures + // that do not apply to a particular sprite. + + vec4 color=vec4(1.,0,0,1.); + + // GLSL is templated out to pick the right texture and set the vec4 color + vec2 uv = u_pixelart ? uv_iq(v_texcoord, v_res) : v_texcoord; + uv.x = remap(0.,1., v_uv_min.x, v_uv_max.x, uv.x); + uv.y = remap(0.,1., v_uv_min.y, v_uv_max.y, uv.y); + + %%texture_picker%% + + color.rgb = color.rgb * v_opacity; + color.a = color.a * v_opacity; + fragColor = color * v_tint; +} \ No newline at end of file diff --git a/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.ts b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.ts new file mode 100644 index 000000000..dcbad2f25 --- /dev/null +++ b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.ts @@ -0,0 +1,435 @@ +// import { Color } from '../../../Color'; +// import { parseImageFiltering } from '../../Filtering'; +import { Color } from '../../../Color'; +import { parseImageFiltering } from '../../Filtering'; +import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; +import { ImageSourceAttributeConstants } from '../../ImageSource'; +import { parseImageWrapping } from '../../Wrapping'; +// import { ImageSourceAttributeConstants } from '../../ImageSource'; +// import { parseImageWrapping } from '../../Wrapping'; +import { HTMLImageSource } from '../ExcaliburGraphicsContext'; +import { ExcaliburGraphicsContextWebGL, pixelSnapEpsilon } from '../ExcaliburGraphicsContextWebGL'; +import { RendererPlugin } from '../renderer'; +import { Shader } from '../shader'; +import { VertexBuffer } from '../vertex-buffer'; +import { getMaxShaderComplexity } from '../webgl-util'; +import frag from './image-renderer-v2.frag.glsl'; +import vert from './image-renderer-v2.vert.glsl'; + +export interface ImageRendererOptions { + pixelArtSampler: boolean; + uvPadding: number; +} + +export class ImageRendererV2 implements RendererPlugin { + public readonly type = 'ex.image-v2'; + public priority: number = 0; + + public readonly pixelArtSampler: boolean; + public readonly uvPadding: number; + + // TODO this could be bigger probably + private _maxImages: number = 20_000; // max(uint16) / 6 verts + private _maxTextures: number = 0; + private _components = 2 + 2 + 2 + 2 + 1 + 2 + 2 + 1 + 2 + 2 + 4; + + private _context!: ExcaliburGraphicsContextWebGL; + private _gl!: WebGL2RenderingContext; + private _shader!: Shader; + private _transformData!: VertexBuffer; + + // Per flush vars + private _imageCount: number = 0; + private _textures: WebGLTexture[] = []; + private _textureIndex = 0; + private _textureToIndex = new Map(); + private _images = new Set(); + private _vertexIndex: number = 0; + private _quadMesh!: Float32Array; + private _meshBuffer!: WebGLBuffer; + private _vao!: WebGLVertexArrayObject; + + constructor(options: ImageRendererOptions) { + this.pixelArtSampler = options.pixelArtSampler; + this.uvPadding = options.uvPadding; + } + + initialize(gl: WebGL2RenderingContext, context: ExcaliburGraphicsContextWebGL): void { + this._gl = gl; + this._context = context; + // Transform shader source + const maxTexture = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); + const maxComplexity = getMaxShaderComplexity(gl, maxTexture); + this._maxTextures = Math.min(maxTexture, maxComplexity); + const transformedFrag = this._transformFragmentSource(frag, this._maxTextures); + // Compile shader + this._shader = new Shader({ + gl, + fragmentSource: transformedFrag, + vertexSource: vert + }); + this._shader.compile(); + + // setup uniforms + this._shader.use(); + this._shader.setUniformMatrix('u_matrix', context.ortho); + // Initialize texture slots to [0, 1, 2, 3, 4, .... maxGPUTextures] + this._shader.setUniformIntArray( + 'u_textures', + [...Array(this._maxTextures)].map((_, i) => i) + ); + + this._vao = gl.createVertexArray()!; + gl.bindVertexArray(this._vao); + this._quadMesh = new Float32Array([ + // pos uv + 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, + + 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1 + ]); + this._meshBuffer = gl.createBuffer()!; + gl.bindBuffer(gl.ARRAY_BUFFER, this._meshBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._quadMesh, gl.STATIC_DRAW); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0); + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8); + gl.enableVertexAttribArray(1); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + // Setup memory layout + const components = this._components; + this._transformData = new VertexBuffer({ + gl, + size: components * this._maxImages, // components * images + type: 'dynamic' + }); + + this._transformData.bind(); + + // attributes + let offset = 0; + let start = 2; + const bytesPerFloat = 4; + const totalSize = components * 4; + + // a_offset vec2 - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(2); + offset += 2 * bytesPerFloat; + + // a_mat_column1 vec2 - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(3); + offset += 2 * bytesPerFloat; + + // a_mat_column2 vec2 - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(4); + offset += 2 * bytesPerFloat; + + // a_mat_column3 vec2 - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(5); + offset += 2 * bytesPerFloat; + + // a_opacity float - 1 + gl.vertexAttribPointer(start++, 1, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(6); + offset += 1 * bytesPerFloat; + + // a_res vec2 - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(7); + offset += 2 * bytesPerFloat; + + // a_size vec2 - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(8); + offset += 2 * bytesPerFloat; + + // a_texture_index - 1 + gl.vertexAttribPointer(start++, 1, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(9); + offset += 1 * bytesPerFloat; + + // a_uv_min - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(10); + offset += 2 * bytesPerFloat; + + // a_uv_max - 2 + gl.vertexAttribPointer(start++, 2, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(11); + offset += 2 * bytesPerFloat; + + // a_tint - 4 + gl.vertexAttribPointer(start++, 4, gl.FLOAT, false, totalSize, offset); + gl.enableVertexAttribArray(12); + offset += 4 * bytesPerFloat; + + gl.vertexAttribDivisor(2, 1); + gl.vertexAttribDivisor(3, 1); + gl.vertexAttribDivisor(4, 1); + gl.vertexAttribDivisor(5, 1); + gl.vertexAttribDivisor(6, 1); + gl.vertexAttribDivisor(7, 1); + gl.vertexAttribDivisor(8, 1); + gl.vertexAttribDivisor(9, 1); + gl.vertexAttribDivisor(10, 1); + gl.vertexAttribDivisor(11, 1); + gl.vertexAttribDivisor(12, 1); + + gl.bindVertexArray(null); + } + + private _bindData(gl: WebGL2RenderingContext) { + // Setup memory layout + const components = this._components; + this._transformData.bind(); + this._transformData.upload(components * this._imageCount); + + gl.bindVertexArray(this._vao); + } + + public dispose() { + this._transformData.dispose(); + this._shader.dispose(); + this._textures.length = 0; + this._context = null as any; + this._gl = null as any; + } + + private _transformFragmentSource(source: string, maxTextures: number): string { + let newSource = source.replace('%%count%%', maxTextures.toString()); + let texturePickerBuilder = ''; + for (let i = 0; i < maxTextures; i++) { + if (i === 0) { + texturePickerBuilder += `if (v_texture_index <= ${i}.5) {\n`; + } else { + texturePickerBuilder += ` else if (v_texture_index <= ${i}.5) {\n`; + } + texturePickerBuilder += ` color = texture(u_textures[${i}], uv);\n`; + texturePickerBuilder += ` }\n`; + } + newSource = newSource.replace('%%texture_picker%%', texturePickerBuilder); + return newSource; + } + + private _addImageAsTexture(image: HTMLImageSource) { + if (this._images.has(image)) { + return; + } + const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering); + const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : undefined; + const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX) as any); + const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY) as any); + + const force = image.getAttribute('forceUpload') === 'true' ? true : false; + const texture = this._context.textureLoader.load( + image, + { + filtering, + wrapping: { x: wrapX, y: wrapY } + }, + force + )!; + // remove force attribute after upload + image.removeAttribute('forceUpload'); + if (this._textures.indexOf(texture) === -1) { + this._textures.push(texture); + this._textureToIndex.set(texture, this._textureIndex++); + this._images.add(image); + } + } + + private _bindTextures(gl: WebGLRenderingContext) { + // Bind textures in the correct order + const max = Math.min(this._textureIndex, this._maxTextures); + for (let i = 0; i < max; i++) { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, this._textures[i] || this._textures[0]); + } + } + + private _getTextureIdForImage(image: HTMLImageSource) { + if (image) { + const maybeTexture = this._context.textureLoader.get(image); + return this._textureToIndex.get(maybeTexture) ?? -1; //this._textures.indexOf(maybeTexture); + } + return -1; + } + + private _isFull() { + if (this._imageCount >= this._maxImages) { + return true; + } + if (this._textures.length >= this._maxTextures) { + return true; + } + return false; + } + + private _imageToWidth = new Map(); + private _getImageWidth(image: HTMLImageSource) { + let maybeWidth = this._imageToWidth.get(image); + if (maybeWidth === undefined) { + maybeWidth = image.width; + this._imageToWidth.set(image, maybeWidth); + } + return maybeWidth; + } + + private _imageToHeight = new Map(); + private _getImageHeight(image: HTMLImageSource) { + let maybeHeight = this._imageToHeight.get(image); + if (maybeHeight === undefined) { + maybeHeight = image.height; + this._imageToHeight.set(image, maybeHeight); + } + return maybeHeight; + } + + private _view = [0, 0, 0, 0]; + private _dest = [0, 0]; + private _defaultTint = Color.White; + draw( + image: HTMLImageSource, + sx: number, + sy: number, + swidth?: number, + sheight?: number, + dx?: number, + dy?: number, + dwidth?: number, + dheight?: number + ): void { + // Force a render if the batch is full + if (this._isFull()) { + this.flush(); + } + + this._imageCount++; + // This creates and uploads the texture if not already done + this._addImageAsTexture(image); + const maybeImageWidth = this._getImageWidth(image); + const maybeImageHeight = this._getImageHeight(image); + + let width = maybeImageWidth || swidth || 0; + let height = maybeImageHeight || sheight || 0; + this._view[0] = 0; + this._view[1] = 0; + this._view[2] = swidth ?? maybeImageWidth ?? 0; + this._view[3] = sheight ?? maybeImageHeight ?? 0; + this._dest[0] = sx ?? 1; + this._dest[1] = sy ?? 1; + // If destination is specified, update view and dest + if (dx !== undefined && dy !== undefined && dwidth !== undefined && dheight !== undefined) { + this._view[0] = sx ?? 1; + this._view[1] = sy ?? 1; + this._view[2] = swidth ?? maybeImageWidth ?? 0; + this._view[3] = sheight ?? maybeImageHeight ?? 0; + this._dest[0] = dx; + this._dest[1] = dy; + width = dwidth; + height = dheight; + } + + sx = this._view[0]; + sy = this._view[1]; + const sw = this._view[2]; + const sh = this._view[3]; + + // transform based on current context + const transform = this._context.getTransform(); + const opacity = this._context.opacity; + const snapToPixel = this._context.snapToPixel; + if (snapToPixel) { + this._dest[0] = ~~(this._dest[0] + pixelSnapEpsilon); + this._dest[1] = ~~(this._dest[1] + pixelSnapEpsilon); + } + + const tint = this._context.tint || this._defaultTint; + + const textureId = this._getTextureIdForImage(image); + const imageWidth = maybeImageWidth || width; + const imageHeight = maybeImageHeight || height; + + const uvx0 = (sx + this.uvPadding) / imageWidth; + const uvy0 = (sy + this.uvPadding) / imageHeight; + const uvx1 = (sx + sw - this.uvPadding) / imageWidth; + const uvy1 = (sy + sh - this.uvPadding) / imageHeight; + + const txWidth = maybeImageWidth; + const txHeight = maybeImageHeight; + + // update data + const vertexBuffer = this._transformData.bufferData; + vertexBuffer[this._vertexIndex++] = this._dest[0]; + vertexBuffer[this._vertexIndex++] = this._dest[1]; + vertexBuffer[this._vertexIndex++] = transform.data[0]; + vertexBuffer[this._vertexIndex++] = transform.data[1]; + vertexBuffer[this._vertexIndex++] = transform.data[2]; + vertexBuffer[this._vertexIndex++] = transform.data[3]; + vertexBuffer[this._vertexIndex++] = transform.data[4]; + vertexBuffer[this._vertexIndex++] = transform.data[5]; + vertexBuffer[this._vertexIndex++] = opacity; + vertexBuffer[this._vertexIndex++] = width; + vertexBuffer[this._vertexIndex++] = height; + vertexBuffer[this._vertexIndex++] = txWidth; + vertexBuffer[this._vertexIndex++] = txHeight; + vertexBuffer[this._vertexIndex++] = textureId; + vertexBuffer[this._vertexIndex++] = uvx0; + vertexBuffer[this._vertexIndex++] = uvy0; + vertexBuffer[this._vertexIndex++] = uvx1; + vertexBuffer[this._vertexIndex++] = uvy1; + vertexBuffer[this._vertexIndex++] = tint.r / 255; + vertexBuffer[this._vertexIndex++] = tint.g / 255; + vertexBuffer[this._vertexIndex++] = tint.b / 255; + vertexBuffer[this._vertexIndex++] = tint.a; + } + + hasPendingDraws(): boolean { + return this._imageCount !== 0; + } + + flush(): void { + // nothing to draw early exit + if (this._imageCount === 0) { + return; + } + + const gl = this._gl; + + // Bind the shader + this._shader.use(); + + // Bind the memory layout and upload data + this._bindData(gl); + + // Update ortho matrix uniform + this._shader.setUniformMatrix('u_matrix', this._context.ortho); + + // Turn on pixel art aa sampler + this._shader.setUniformBoolean('u_pixelart', this.pixelArtSampler); + + // Bind textures to + this._bindTextures(gl); + + // Draw all the quads + gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this._imageCount); + + GraphicsDiagnostics.DrawnImagesCount += this._imageCount; + GraphicsDiagnostics.DrawCallCount++; + + gl.bindVertexArray(null); + // Reset + this._imageCount = 0; + this._vertexIndex = 0; + this._textures.length = 0; + this._textureIndex = 0; + this._textureToIndex.clear(); + this._images.clear(); + this._imageToWidth.clear(); + this._imageToHeight.clear(); + } +} diff --git a/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.vert.glsl b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.vert.glsl new file mode 100644 index 000000000..b1fddcfda --- /dev/null +++ b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.vert.glsl @@ -0,0 +1,53 @@ +#version 300 es +layout(location=0) in vec2 pos; +layout(location=1) in vec2 a_texcoord; +out vec2 v_texcoord; + +layout(location=2) in vec2 a_offset; +layout(location=3) in vec2 a_mat_column1; +layout(location=4) in vec2 a_mat_column2; +layout(location=5) in vec2 a_mat_column3; + +layout(location=6) in float a_opacity; +out float v_opacity; + +// Texture resolution (could be bigger than a_siz) +layout(location=7) in vec2 a_res; +out vec2 v_res; + +// Final size of graphic +layout(location=8) in vec2 a_size; + +layout(location=9) in lowp float a_texture_index; +out lowp float v_texture_index; + +layout(location=10) in vec2 a_uv_min; +out vec2 v_uv_min; + +layout(location=11) in vec2 a_uv_max; +out vec2 v_uv_max; + +layout(location=12) in vec4 a_tint; +out vec4 v_tint; + +uniform mat4 u_matrix; + +void main(){ + mat4 world_mat = mat4( + a_mat_column1.x, a_mat_column1.y, 0., 0., + a_mat_column2.x, a_mat_column2.y, 0., 0., + 0. , 0. , 1., 0., + a_mat_column3.x, a_mat_column3.y, 0., 1. + ); + + vec2 newPos = vec2(pos.x * a_res.x, pos.y * a_res.y); + gl_Position = u_matrix * world_mat * vec4(newPos + a_offset, 0., 1.); + + v_opacity = a_opacity; + v_texcoord = a_texcoord; + v_uv_min = a_uv_min; + v_uv_max = a_uv_max; + v_res = a_res; + v_texture_index = a_texture_index; + v_tint = a_tint; +} \ No newline at end of file diff --git a/src/engine/Graphics/Context/vertex-layout.ts b/src/engine/Graphics/Context/vertex-layout.ts index 153bc55eb..4e5f3b33b 100644 --- a/src/engine/Graphics/Context/vertex-layout.ts +++ b/src/engine/Graphics/Context/vertex-layout.ts @@ -16,12 +16,16 @@ export interface VertexLayoutOptions { * Vertex buffer to use for vertex data */ vertexBuffer: VertexBuffer; + /** + * Starting index for the attribute pointer + */ + attributePointerStartIndex?: number; /** * Specify the attributes that will exist in the vertex buffer * * **Important** must specify them in the order that they will be in the vertex buffer!! */ - attributes: [name: string, numberOfComponents: number][]; + attributes: [name: string, numberOfComponents: number, type?: 'int' | 'matrix' | 'float'][]; /** * Optionally suppress any warnings out of vertex layouts * @@ -44,7 +48,7 @@ export class VertexLayout { private _suppressWarnings = false; private _shader: Shader; private _layout: VertexAttributeDefinition[] = []; - private _attributes: [name: string, numberOfComponents: number][] = []; + private _attributes: [name: string, numberOfComponents: number, type?: 'int' | 'matrix' | 'float'][] = []; private _vertexBuffer: VertexBuffer; private _vao!: WebGLVertexArrayObject; @@ -52,7 +56,7 @@ export class VertexLayout { return this._vertexBuffer; } - public get attributes(): readonly [name: string, numberOfComponents: number][] { + public get attributes(): readonly [name: string, numberOfComponents: number, type?: 'int' | 'matrix' | 'float'][] { return this._attributes; } @@ -172,9 +176,13 @@ export class VertexLayout { let offset = 0; for (const vert of this._layout) { + // skip unused attributes if (vert.location !== -1) { - // skip unused attributes - gl.vertexAttribPointer(vert.location, vert.size, vert.glType, vert.normalized, this.totalVertexSizeBytes, offset); + if (vert.glType === gl.INT) { + gl.vertexAttribIPointer(vert.location, vert.size, vert.glType, this.totalVertexSizeBytes, offset); + } else { + gl.vertexAttribPointer(vert.location, vert.size, vert.glType, vert.normalized, this.totalVertexSizeBytes, offset); + } gl.enableVertexAttribArray(vert.location); } offset += getGlTypeSizeBytes(gl, vert.glType) * vert.size; diff --git a/src/engine/Graphics/Context/webgl-util.ts b/src/engine/Graphics/Context/webgl-util.ts index 6027059cc..1a26f57ce 100644 --- a/src/engine/Graphics/Context/webgl-util.ts +++ b/src/engine/Graphics/Context/webgl-util.ts @@ -5,6 +5,8 @@ */ export function getGlTypeSizeBytes(gl: WebGLRenderingContext, type: number): number { switch (type) { + case gl.INT: + case gl.UNSIGNED_INT: case gl.FLOAT: return 4; case gl.SHORT: diff --git a/src/engine/Util/Pool.ts b/src/engine/Util/Pool.ts index 805497872..d18415a97 100644 --- a/src/engine/Util/Pool.ts +++ b/src/engine/Util/Pool.ts @@ -8,7 +8,7 @@ export class Pool { constructor( public builder: () => Type, - public recycler: (instance: Type) => Type, + public recycler?: (instance: Type) => Type, public maxObjects: number = 100 ) {} @@ -59,7 +59,10 @@ export class Pool { if (this.objects[this.index]) { // Pool has an available object already constructed - return this.recycler(this.objects[this.index++]); + if (this.recycler) { + return this.recycler(this.objects[this.index++]); + } + return this.objects[this.index++]; } else { // New allocation this.totalAllocations++; diff --git a/src/spec/ExcaliburGraphicsContextSpec.ts b/src/spec/ExcaliburGraphicsContextSpec.ts index e25b1e8c8..df35cdda2 100644 --- a/src/spec/ExcaliburGraphicsContextSpec.ts +++ b/src/spec/ExcaliburGraphicsContextSpec.ts @@ -552,6 +552,63 @@ describe('The ExcaliburGraphicsContext', () => { const tex = new ex.ImageSource('src/spec/images/ExcaliburGraphicsContextSpec/sword.png'); await tex.load(); + const circleRenderer = sut.get('ex.circle'); + spyOn(circleRenderer, 'flush').and.callThrough(); + const imageRenderer = sut.get('ex.image-v2'); + spyOn(imageRenderer, 'flush').and.callThrough(); + const rectangleRenderer = sut.get('ex.rectangle'); + spyOn(rectangleRenderer, 'flush').and.callThrough(); + + sut.clear(); + + sut.useDrawSorting = false; + + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); + expect(rectangleRenderer.flush).withContext('rectangle line render not flushed yet').not.toHaveBeenCalled(); + + sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); + expect(circleRenderer.flush).withContext('circle is batched not flushed yet').not.toHaveBeenCalled(); + expect(rectangleRenderer.flush).withContext('rectangle line render').toHaveBeenCalled(); + + sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); + expect(circleRenderer.flush).withContext('circle renderer switched, flush required').toHaveBeenCalled(); + expect(imageRenderer.flush).withContext('image batched not yet flushed').not.toHaveBeenCalled(); + + sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); + expect(imageRenderer.flush).toHaveBeenCalled(); + + sut.flush(); + + // Rectangle renderer handles lines and rectangles why it's twice + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); + expect(circleRenderer.flush).toHaveBeenCalledTimes(1); + expect(imageRenderer.flush).toHaveBeenCalledTimes(1); + + await expectAsync(canvasElement).toEqualImage( + 'src/spec/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', + 0.97 + ); + sut.dispose(); + }); + + it('(legacy image renderer) will preserve the painter order when switching renderer (no draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White, + snapToPixel: false + }); + sut.imageRenderer = 'ex.image'; + + const tex = new ex.ImageSource('src/spec/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); + const circleRenderer = sut.get('ex.circle'); spyOn(circleRenderer, 'flush').and.callThrough(); const imageRenderer = sut.get('ex.image'); @@ -608,6 +665,59 @@ describe('The ExcaliburGraphicsContext', () => { const tex = new ex.ImageSource('src/spec/images/ExcaliburGraphicsContextSpec/sword.png'); await tex.load(); + const circleRenderer = sut.get('ex.circle'); + spyOn(circleRenderer, 'flush').and.callThrough(); + const imageRenderer = sut.get('ex.image-v2'); + spyOn(imageRenderer, 'flush').and.callThrough(); + const rectangleRenderer = sut.get('ex.rectangle'); + spyOn(rectangleRenderer, 'flush').and.callThrough(); + + sut.clear(); + sut.useDrawSorting = true; + + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); + + sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); + + sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); + + // With draw sorting on, the rectangle at z=0 is part of the draw line, so setting the z = 1 forces the desired effect + sut.save(); + sut.z = 1; + sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); + sut.restore(); + + sut.flush(); + + // Rectangle renderer handles lines and rectangles why it's twice + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); + expect(circleRenderer.flush).toHaveBeenCalledTimes(1); + expect(imageRenderer.flush).toHaveBeenCalledTimes(1); + + await expectAsync(canvasElement).toEqualImage( + 'src/spec/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', + 0.97 + ); + sut.dispose(); + }); + it('(legacy image renderer) will preserve the painter order when switching renderer (draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White, + snapToPixel: false + }); + sut.imageRenderer = 'ex.image'; + + const tex = new ex.ImageSource('src/spec/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); + const circleRenderer = sut.get('ex.circle'); spyOn(circleRenderer, 'flush').and.callThrough(); const imageRenderer = sut.get('ex.image');