diff --git a/CHANGELOG.md b/CHANGELOG.md index 8800b2fa1..4f38b916a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,21 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added new `new ex.Engine({uvPadding: .25})` option to allow users using texture atlases in their sprite sheets to configure this to avoid texture bleed. This can happen if you're sampling from images meant for pixel art +- Added new antialias settings for pixel art! This allows for smooth subpixel rendering of pixel art without shimmer/fat-pixel artifacts. + - Use `new ex.Engine({pixelArt: true})` to opt in to all the right defaults to make this work! +- Added new antialias configuration options to deeply configure how Excalibur does any antialiasing, or you can provide `antialiasing: true`/`antialiasing: false` to use the old defaults. + - Example; + ```typescript + const game = new ex.Engine({ + antialiasing: { + pixelArtSampler: false, + filtering: ex.ImageFiltering.Pixel, + nativeContextAntialiasing: false, + canvasImageRendering: 'pixelated' + } + }) + ``` - Added new `lineHeight` property on `SpriteFont` and `Font` to manually adjust the line height when rendering text. - Added missing dual of `ex.GraphicsComponent.add()`, you can now `ex.GraphicsComponent.remove(name)`; - Added additional options to `ex.Animation.fromSpriteSheetCoordinates()` you can now pass any valid `ex.GraphicOptions` to influence the sprite per frame @@ -156,6 +171,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed issue with `snapToPixel` where the `ex.Camera` was not snapping correctly - Fixed issue where using CSS transforms on the canvas confused Excalibur pointers - Fixed issue with *AndFill suffixed [[DisplayModes]]s where content area offset was not accounted for in world space - Fixed issue where `ex.Sound.getTotalPlaybackDuration()` would crash if not loaded, now logs friendly warning diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index 26be9c515..e4553eaf0 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -52,13 +52,13 @@ var game = new ex.Engine({ snapToPixel: false, fixedUpdateFps: 30, maxFps: 60, + antialiasing: false, configurePerformanceCanvas2DFallback: { allow: true, showPlayerMessage: true, threshold: { fps: 20, numberOfFrames: 100 } } }); -game.setAntialiasing(false); game.screen.events.on('fullscreen', (evt) => { console.log('fullscreen', evt); }); diff --git a/sandbox/tests/graphicscontext/index.html b/sandbox/tests/graphicscontext/index.html new file mode 100644 index 000000000..cfde743e6 --- /dev/null +++ b/sandbox/tests/graphicscontext/index.html @@ -0,0 +1,12 @@ + + + + + + Graphics Context + + + + + + \ No newline at end of file diff --git a/sandbox/tests/graphicscontext/index.ts b/sandbox/tests/graphicscontext/index.ts new file mode 100644 index 000000000..b9d33203e --- /dev/null +++ b/sandbox/tests/graphicscontext/index.ts @@ -0,0 +1,30 @@ +var canvasElement = document.createElement('canvas'); +document.body.appendChild(canvasElement); +canvasElement.width = 100; +canvasElement.height = 100; +var sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: true, + backgroundColor: ex.Color.White +}); +sut.updateViewport({ + width: 100, + height: 100 +}); + +var rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Blue +}); +sut.beginDrawLifecycle(); +sut.clear(); + +sut.save() +sut.drawImage(rect._bitmap, 20, 20); +sut.restore(); + +sut.flush(); +sut.endDrawLifecycle(); diff --git a/sandbox/tests/raycast/main.ts b/sandbox/tests/raycast/main.ts index 0c7b34698..74e3502be 100644 --- a/sandbox/tests/raycast/main.ts +++ b/sandbox/tests/raycast/main.ts @@ -13,7 +13,7 @@ var notPlayers2 = playerGroup.invert(); var player = new ex.Actor({ name: 'player', - pos: ex.vec(100, 100), + pos: ex.vec(70, 320), width: 40, height: 40, collisionGroup: playerGroup, diff --git a/sandbox/tests/sprite/sprite.ts b/sandbox/tests/sprite/sprite.ts index 21ced233a..36faeb27c 100644 --- a/sandbox/tests/sprite/sprite.ts +++ b/sandbox/tests/sprite/sprite.ts @@ -3,9 +3,13 @@ var game = new ex.Engine({ canvasElementId: 'game', width: 600, - height: 400 + height: 400, + displayMode: ex.DisplayMode.FitScreenAndFill, + pixelArt: true, + // antialiasing: false }); + var tex = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png'); var loader = new ex.Loader([tex]); @@ -23,3 +27,7 @@ actor.onInitialize = () => { }; game.add(actor); game.start(loader); + +game.currentScene.camera.pos = actor.pos; +game.currentScene.camera.zoom = 7; +actor.angularVelocity = .1; diff --git a/src/engine/Camera.ts b/src/engine/Camera.ts index 2524f8c6a..5d05cf6ed 100644 --- a/src/engine/Camera.ts +++ b/src/engine/Camera.ts @@ -12,6 +12,8 @@ import { ExcaliburGraphicsContext } from './Graphics/Context/ExcaliburGraphicsCo import { watchAny } from './Util/Watch'; import { AffineMatrix } from './Math/affine-matrix'; import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; +import { pixelSnapEpsilon } from './Graphics'; +import { sign } from './Math/util'; /** * Interface that describes a custom camera strategy for tracking targets @@ -776,6 +778,7 @@ export class Camera implements CanUpdate, CanInitialize { this._postupdate(engine, delta); } + private _snapPos = vec(0, 0); /** * Applies the relevant transformations to the game canvas to "move" or apply effects to the Camera * @param ctx Canvas context to apply transformations @@ -794,7 +797,14 @@ export class Camera implements CanUpdate, CanInitialize { interpolatedPos.clone(this.drawPos); this.updateTransform(interpolatedPos); } - + // Snap camera to pixel + if (ctx.snapToPixel) { + const snapPos = this.drawPos.clone(this._snapPos); + snapPos.x = ~~(snapPos.x + pixelSnapEpsilon * sign(snapPos.x)); + snapPos.y = ~~(snapPos.y + pixelSnapEpsilon * sign(snapPos.y)); + snapPos.clone(this.drawPos); + this.updateTransform(snapPos); + } ctx.multiply(this.transform); } diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index f09737c12..487328dc3 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -35,7 +35,15 @@ import { Scene, SceneConstructor, isSceneConstructor } from './Scene'; import { Entity } from './EntityComponentSystem/Entity'; import { Debug, DebugStats } from './Debug/Debug'; import { BrowserEvents } from './Util/Browser'; -import { ExcaliburGraphicsContext, ExcaliburGraphicsContext2DCanvas, ExcaliburGraphicsContextWebGL, TextureLoader } from './Graphics'; +import { + AntialiasOptions, + DefaultAntialiasOptions, + DefaultPixelArtOptions, + ExcaliburGraphicsContext, + ExcaliburGraphicsContext2DCanvas, + ExcaliburGraphicsContextWebGL, + TextureLoader +} from './Graphics'; import { Clock, StandardClock } from './Util/Clock'; import { ImageFiltering } from './Graphics/Filtering'; import { GraphicsDiagnostics } from './Graphics/GraphicsDiagnostics'; @@ -74,6 +82,8 @@ export const EngineEvents = { PostDraw: 'postdraw' } as const; + + /** * Enum representing the different mousewheel event bubble prevention */ @@ -122,12 +132,49 @@ export interface EngineOptions { * Optionally specify antialiasing (smoothing), by default true (smooth pixels) * * * `true` - useful for high resolution art work you would like smoothed, this also hints excalibur to load images - * with [[ImageFiltering.Blended]] + * with default blending [[ImageFiltering.Blended]] * * * `false` - useful for pixel art style art work you would like sharp, this also hints excalibur to load images - * with [[ImageFiltering.Pixel]] + * with default blending [[ImageFiltering.Pixel]] + * + * * [[AntialiasOptions]] Optionally deeply configure the different antialiasing settings, **WARNING** thar be dragons here. + * It is recommended you stick to `true` or `false` unless you understand what you're doing and need to control rendering to + * a high degree. + */ + antialiasing?: boolean | AntialiasOptions + + /** + * Quick convenience property to configure Excalibur to use special settings for "pretty" anti-aliased pixel art + * + * 1. Turns on special shader condition to blend for pixel art and enables various antialiasing settings, + * notice blending is ON for this special mode. + * + * Equivalent to: + * ```javascript + * antialiasing: { + * pixelArtSampler: true, + * canvasImageRendering: 'auto', + * filtering: ImageFiltering.Blended, + * webglAntialiasing: true + * } + * ``` + */ + pixelArt?: boolean; + + /** + * Specify any UV padding you want use in pixels, this brings sampling into the texture if you're using + * a sprite sheet in one image to prevent sampling bleed. + * + * By default .01 pixels, and .25 pixels if `pixelArt: true` */ - antialiasing?: boolean; + uvPadding?: number; + + /** + * Optionally hint the graphics context into a specific power profile + * + * Default "high-performance" + */ + powerPreference?: 'default' | 'high-performance' | 'low-power'; /** * Optionally upscale the number of pixels in the canvas. Normally only useful if you need a smoother look to your assets, especially @@ -601,6 +648,9 @@ export class Engine implements CanInitialize, canvasElementId: '', canvasElement: undefined, snapToPixel: false, + antialiasing: true, + pixelArt: false, + powerPreference: 'high-performance', pointerScope: PointerScope.Canvas, suppressConsoleBootMessage: null, suppressMinimumBrowserFeatureDetection: null, @@ -735,6 +785,43 @@ O|===|* >________________>\n\ this._originalDisplayMode = displayMode; + let pixelArtSampler: boolean; + let uvPadding: number; + let nativeContextAntialiasing: boolean; + let canvasImageRendering: 'pixelated' | 'auto'; + let filtering: ImageFiltering; + let multiSampleAntialiasing: boolean | { samples: number }; + if (typeof options.antialiasing === 'object') { + ({ + pixelArtSampler, + nativeContextAntialiasing, + multiSampleAntialiasing, + filtering, + canvasImageRendering + } = { + ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions), + ...options.antialiasing + }); + + } else { + pixelArtSampler = !!options.pixelArt; + nativeContextAntialiasing = false; + multiSampleAntialiasing = options.antialiasing; + canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated'; + filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel; + } + + if (nativeContextAntialiasing && multiSampleAntialiasing) { + this._logger.warnOnce(`Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` + + ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing`); + } + + if (options.pixelArt) { + uvPadding = .25; + } + // Override with any user option, if non default to .25 for pixel art, 0.01 for everything else + uvPadding = options.uvPadding ?? uvPadding ?? 0.01; + // Canvas 2D fallback can be flagged on let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context'); if (!useCanvasGraphicsContext) { @@ -743,7 +830,11 @@ O|===|* >________________>\n\ this.graphicsContext = new ExcaliburGraphicsContextWebGL({ canvasElement: this.canvas, enableTransparency: this.enableCanvasTransparency, - smoothing: options.antialiasing, + pixelArtSampler: pixelArtSampler, + antialiasing: nativeContextAntialiasing, + multiSampleAntialiasing: multiSampleAntialiasing, + uvPadding: uvPadding, + powerPreference: options.powerPreference, backgroundColor: options.backgroundColor, snapToPixel: options.snapToPixel, useDrawSorting: options.useDrawSorting @@ -763,7 +854,7 @@ O|===|* >________________>\n\ this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({ canvasElement: this.canvas, enableTransparency: this.enableCanvasTransparency, - smoothing: options.antialiasing, + antialiasing: nativeContextAntialiasing, backgroundColor: options.backgroundColor, snapToPixel: options.snapToPixel, useDrawSorting: options.useDrawSorting @@ -773,7 +864,8 @@ O|===|* >________________>\n\ this.screen = new Screen({ canvas: this.canvas, context: this.graphicsContext, - antialiasing: options.antialiasing ?? true, + antialiasing: nativeContextAntialiasing, + canvasImageRendering: canvasImageRendering, browser: this.browser, viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA), resolution: options.resolution, @@ -783,7 +875,7 @@ O|===|* >________________>\n\ // TODO REMOVE STATIC!!! // Set default filtering based on antialiasing - TextureLoader.filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel; + TextureLoader.filtering = filtering; if (options.backgroundColor) { this.backgroundColor = options.backgroundColor.clone(); @@ -883,7 +975,7 @@ O|===|* >________________>\n\ this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({ canvasElement: this.canvas, enableTransparency: this.enableCanvasTransparency, - smoothing: options.antialiasing, + antialiasing: options.antialiasing, backgroundColor: options.backgroundColor, snapToPixel: options.snapToPixel, useDrawSorting: options.useDrawSorting @@ -1189,6 +1281,7 @@ O|===|* >________________>\n\ * canvas. Set this to `false` if you want a 'jagged' pixel art look to your * image resources. * @param isSmooth Set smoothing to true or false + * @deprecated Set in engine constructor, will be removed in v0.30 */ public setAntialiasing(isSmooth: boolean) { this.screen.antialiasing = isSmooth; @@ -1196,6 +1289,7 @@ O|===|* >________________>\n\ /** * Return the current smoothing status of the canvas + * @deprecated Set in engine constructor, will be removed in v0.30 */ public getAntialiasing(): boolean { return this.screen.antialiasing; diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts index 7fb2e3d3f..56032bad4 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts @@ -4,15 +4,122 @@ import { ScreenDimension } from '../../Screen'; import { PostProcessor } from '../PostProcessor/PostProcessor'; import { AffineMatrix } from '../../Math/affine-matrix'; import { Material, MaterialOptions } from './material'; +import { ImageFiltering } from '../Filtering'; export type HTMLImageSource = HTMLImageElement | HTMLCanvasElement; +export interface AntialiasOptions { + /** + * Turns on the special pixel art sampler in excalibur's image shader for sub pixel + * anti-aliasing + * + * Default false + */ + pixelArtSampler?: boolean; + /** + * Configures the webgl's getContext('webgl2', {antialias: true | false}) or configures + * Canvas2D imageSmoothing = true; + * + * **Note** this option is incompatible with `multiSampleAntialiasing` + * + * Default false + */ + nativeContextAntialiasing?: boolean; + /** + * Configures the internal render buffer multi-sampling settings + * + * Default true, with max samples that the platform supports + */ + multiSampleAntialiasing?: boolean | { + /** + * Optionally specify number of samples (will be clamped to the max the platform supports) + * + * Default most platforms are 16 samples + */ + samples: number; + }; + /** + * Sets the default image filtering for excalibur + * + * Default [[ImageFiltering.Blended]] + */ + filtering?: ImageFiltering; + /** + * Sets the canvas image rendering CSS style + * + * Default 'auto' + */ + canvasImageRendering?: 'pixelated' | 'auto'; +} + +export const DefaultAntialiasOptions: Required = { + pixelArtSampler: false, + nativeContextAntialiasing: false, + multiSampleAntialiasing: true, + filtering: ImageFiltering.Blended, + canvasImageRendering: 'auto' +}; + +export const DefaultPixelArtOptions: Required = { + pixelArtSampler: true, + nativeContextAntialiasing: false, + multiSampleAntialiasing: true, + filtering: ImageFiltering.Blended, + canvasImageRendering: 'auto' +}; + export interface ExcaliburGraphicsContextOptions { + /** + * Target existing html canvas element + */ canvasElement: HTMLCanvasElement; - smoothing?: boolean; + /** + * Enables antialiasing on the canvas context (smooths pixels with default canvas sampling) + */ + antialiasing?: boolean; + /** + * Enable the sub pixel antialiasing pixel art sampler for nice looking pixel art + */ + pixelArtSampler?: boolean; + /** + * Enable canvas transparency + */ enableTransparency?: boolean; + /** + * Enable or disable multi-sample antialiasing in the internal render buffer. + * + * If true the max number of samples will be used + * + * By default enabled + */ + multiSampleAntialiasing?: boolean | { + /** + * Specify number of samples to use during the multi sample anti-alias, if not specified the max will be used. + * Limited by the hardware (usually 16) + */ + samples: number + }, + /** + * UV padding in pixels to use in the internal image rendering + * + * Recommended .25 - .5 of a pixel + */ + uvPadding?: number; + /** + * Hint the power preference to the graphics context + */ + powerPreference?: 'default' | 'high-performance' | 'low-power'; + /** + * Snaps the pixel to an integer value (floor) + */ snapToPixel?: boolean; + /** + * Current clearing color of the context + */ backgroundColor?: Color; + /** + * Feature flag that enables draw sorting will removed in v0.29 + */ useDrawSorting?: boolean; } @@ -95,7 +202,7 @@ export interface ExcaliburGraphicsContext { snapToPixel: boolean; /** - * Enable smoothed drawing (also known as anti-aliasing), by default false + * Enable smoothed drawing (also known as anti-aliasing), by default true */ smoothing: boolean; diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts index 8e361c833..ea8b4ac3b 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts @@ -134,7 +134,7 @@ export class ExcaliburGraphicsContext2DCanvas implements ExcaliburGraphicsContex } constructor(options: ExcaliburGraphicsContextOptions) { - const { canvasElement, enableTransparency, snapToPixel, smoothing, backgroundColor } = options; + const { canvasElement, enableTransparency, snapToPixel, antialiasing: smoothing, backgroundColor } = options; this.__ctx = canvasElement.getContext('2d', { alpha: enableTransparency ?? true }); diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index 5a55ced21..c50f4caa1 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -106,6 +106,9 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { // Main render target private _renderTarget: RenderTarget; + // Quad boundary MSAA + private _msaaTarget: RenderTarget; + // Postprocessing is a tuple with 2 render targets, these are flip-flopped during the postprocessing process private _postProcessTargets: RenderTarget[] = []; @@ -122,9 +125,27 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { private _state = new StateStack(); private _ortho!: Matrix; + /** + * Snaps the drawing x/y coordinate to the nearest whole pixel + */ public snapToPixel: boolean = false; - public smoothing: boolean = false; + /** + * Native context smoothing + */ + public readonly smoothing: boolean = false; + + + /** + * Whether the pixel art sampler is enabled for smooth sub pixel anti-aliasing + */ + public readonly pixelArtSampler: boolean = false; + + /** + * UV padding in pixels to use in internal image rendering to prevent texture bleed + * + */ + public uvPadding = .01; public backgroundColor: Color = Color.ExcaliburBlue; @@ -181,23 +202,41 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { return supported; } + public readonly multiSampleAntialiasing: boolean = true; + public readonly samples?: number; + public readonly transparency: boolean = true; + constructor(options: ExcaliburGraphicsContextOptions) { - const { canvasElement, enableTransparency, smoothing, snapToPixel, backgroundColor, useDrawSorting } = options; + const { + canvasElement, + enableTransparency, + antialiasing, + uvPadding, + multiSampleAntialiasing, + pixelArtSampler, + powerPreference, + snapToPixel, + backgroundColor, + useDrawSorting + } = options; this.__gl = canvasElement.getContext('webgl2', { - antialias: smoothing ?? this.smoothing, + antialias: antialiasing ?? this.smoothing, premultipliedAlpha: false, - alpha: enableTransparency ?? true, - depth: true, - powerPreference: 'high-performance' - // TODO Chromium fixed the bug where this didn't work now it breaks CI :( - // failIfMajorPerformanceCaveat: true + alpha: enableTransparency ?? this.transparency, + depth: false, + powerPreference: powerPreference ?? 'high-performance' }); if (!this.__gl) { throw Error('Failed to retrieve webgl context from browser'); } this.textureLoader = new TextureLoader(this.__gl); this.snapToPixel = snapToPixel ?? this.snapToPixel; - this.smoothing = smoothing ?? this.smoothing; + this.smoothing = antialiasing ?? this.smoothing; + this.transparency = enableTransparency ?? this.transparency; + this.pixelArtSampler = pixelArtSampler ?? this.pixelArtSampler; + this.uvPadding = uvPadding ?? this.uvPadding; + this.multiSampleAntialiasing = typeof multiSampleAntialiasing === 'boolean' ? multiSampleAntialiasing : this.multiSampleAntialiasing; + this.samples = typeof multiSampleAntialiasing === 'object' ? multiSampleAntialiasing.samples : undefined; this.backgroundColor = backgroundColor ?? this.backgroundColor; this.useDrawSorting = useDrawSorting ?? this.useDrawSorting; this._drawCallPool.disableWarnings = true; @@ -224,7 +263,10 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); // Setup builtin renderers - this.register(new ImageRenderer()); + this.register(new ImageRenderer({ + uvPadding: this.uvPadding, + pixelArtSampler: this.pixelArtSampler + })); this.register(new MaterialRenderer()); this.register(new RectangleRenderer()); this.register(new CircleRenderer()); @@ -245,23 +287,34 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this._renderTarget = new RenderTarget({ gl, + transparency: this.transparency, width: gl.canvas.width, height: gl.canvas.height }); - this._postProcessTargets = [ new RenderTarget({ gl, + transparency: this.transparency, width: gl.canvas.width, height: gl.canvas.height }), new RenderTarget({ gl, + transparency: this.transparency, width: gl.canvas.width, height: gl.canvas.height }) ]; + + this._msaaTarget = new RenderTarget({ + gl, + transparency: this.transparency, + width: gl.canvas.width, + height: gl.canvas.height, + antialias: this.multiSampleAntialiasing, + samples: this.samples + }); } public register(renderer: T) { @@ -341,6 +394,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this._ortho = this._ortho = Matrix.ortho(0, resolution.width, resolution.height, 0, 400, -400); this._renderTarget.setResolution(gl.canvas.width, gl.canvas.height); + this._msaaTarget.setResolution(gl.canvas.width, gl.canvas.height); this._postProcessTargets[0].setResolution(gl.canvas.width, gl.canvas.height); this._postProcessTargets[1].setResolution(gl.canvas.width, gl.canvas.height); } @@ -514,7 +568,8 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { clear() { const gl = this.__gl; - this._renderTarget.use(); + const currentTarget = this.multiSampleAntialiasing ? this._msaaTarget : this._renderTarget; + currentTarget.use(); gl.clearColor(this.backgroundColor.r / 255, this.backgroundColor.g / 255, this.backgroundColor.b / 255, this.backgroundColor.a); // Clear the context with the newly set color. This is // the function call that actually does the drawing. @@ -525,10 +580,9 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { * Flushes all batched rendering to the screen */ flush() { - const gl = this.__gl; - // render target captures all draws and redirects to the render target - this._renderTarget.use(); + let currentTarget = this.multiSampleAntialiasing ? this._msaaTarget : this._renderTarget; + currentTarget.use(); if (this.useDrawSorting) { // sort draw calls @@ -572,10 +626,8 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { // ! hack to grab screen texture before materials run because they might want it if (currentRenderer instanceof MaterialRenderer && this.material.isUsingScreenTexture) { - const gl = this.__gl; - gl.bindTexture(gl.TEXTURE_2D, this.materialScreenTexture); - gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, this.width, this.height, 0); - this._renderTarget.use(); + currentTarget.copyToTexture(this.materialScreenTexture); + currentTarget.use(); } // If we are still using the same renderer we can add to the current batch currentRenderer.draw(...this._drawCalls[i].args); @@ -601,21 +653,23 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { } } - this._renderTarget.disable(); + currentTarget.disable(); // post process step - const source = this._renderTarget.toRenderSource(); - source.use(); + if (this._postprocessors.length > 0) { + const source = currentTarget.toRenderSource(); + source.use(); + } - // flip flop render targets + // flip flop render targets for post processing for (let i = 0; i < this._postprocessors.length; i++) { + currentTarget = this._postProcessTargets[i % 2]; this._postProcessTargets[i % 2].use(); this._screenRenderer.renderWithPostProcessor(this._postprocessors[i]); this._postProcessTargets[i % 2].toRenderSource().use(); } - // passing null switches rendering back to the canvas - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - this._screenRenderer.renderToScreen(); + // Final blit to the screen + currentTarget.blitToScreen(); } } diff --git a/src/engine/Graphics/Context/image-renderer/image-renderer.frag.glsl b/src/engine/Graphics/Context/image-renderer/image-renderer.frag.glsl index e4502dd0b..deaec0e90 100644 --- a/src/engine/Graphics/Context/image-renderer/image-renderer.frag.glsl +++ b/src/engine/Graphics/Context/image-renderer/image-renderer.frag.glsl @@ -10,24 +10,39 @@ in lowp float v_textureIndex; // Textures in the current draw uniform sampler2D u_textures[%%count%%]; +uniform bool u_pixelart; + // Opacity in float v_opacity; in vec4 v_tint; -out vec4 fragColor; - -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. +in vec2 v_res; - vec4 color = vec4(1.0, 0, 0, 1.0); - - // GLSL is templated out to pick the right texture and set the vec4 color - %%texture_picker%% +out vec4 fragColor; - color.rgb = color.rgb * v_opacity; - color.a = color.a * v_opacity; - fragColor = color * v_tint; +// 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; +} + +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 + %%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/image-renderer.ts b/src/engine/Graphics/Context/image-renderer/image-renderer.ts index e822d01cd..e0882ad4f 100644 --- a/src/engine/Graphics/Context/image-renderer/image-renderer.ts +++ b/src/engine/Graphics/Context/image-renderer/image-renderer.ts @@ -1,3 +1,4 @@ +import { sign } from '../../../Math/util'; import { vec } from '../../../Math/vector'; import { ImageFiltering } from '../../Filtering'; import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; @@ -11,10 +12,18 @@ import { VertexLayout } from '../vertex-layout'; import frag from './image-renderer.frag.glsl'; import vert from './image-renderer.vert.glsl'; +export interface ImageRendererOptions { + pixelArtSampler: boolean; + uvPadding: number; +} + export class ImageRenderer implements RendererPlugin { public readonly type = 'ex.image'; public priority: number = 0; + public readonly pixelArtSampler: boolean; + public readonly uvPadding: number; + private _maxImages: number = 10922; // max(uint16) / 6 verts private _maxTextures: number = 0; @@ -30,6 +39,11 @@ export class ImageRenderer implements RendererPlugin { private _textures: WebGLTexture[] = []; private _vertexIndex: number = 0; + constructor(options: ImageRendererOptions) { + this.pixelArtSampler = options.pixelArtSampler; + this.uvPadding = options.uvPadding; + } + initialize(gl: WebGL2RenderingContext, context: ExcaliburGraphicsContextWebGL): void { this._gl = gl; this._context = context; @@ -57,7 +71,7 @@ export class ImageRenderer implements RendererPlugin { // Setup memory layout this._buffer = new VertexBuffer({ gl, - size: 10 * 4 * this._maxImages, // 10 components * 4 verts + size: 12 * 4 * this._maxImages, // 12 components * 4 verts type: 'dynamic' }); this._layout = new VertexLayout({ @@ -67,6 +81,7 @@ export class ImageRenderer implements RendererPlugin { attributes: [ ['a_position', 2], ['a_opacity', 1], + ['a_res', 2], ['a_texcoord', 2], ['a_textureIndex', 1], ['a_tint', 4] @@ -86,7 +101,8 @@ export class ImageRenderer implements RendererPlugin { } else { texturePickerBuilder += ` else if (v_textureIndex <= ${i}.5) {\n`; } - texturePickerBuilder += ` color = texture(u_textures[${i}], v_texcoord);\n`; + texturePickerBuilder += ` vec2 uv = u_pixelart ? uv_iq(v_texcoord, v_res) : v_texcoord;\n`; + texturePickerBuilder += ` color = texture(u_textures[${i}], uv);\n`; texturePickerBuilder += ` }\n`; } newSource = newSource.replace('%%texture_picker%%', texturePickerBuilder); @@ -189,17 +205,17 @@ export class ImageRenderer implements RendererPlugin { bottomRight = transform.multiply(bottomRight); if (snapToPixel) { - topLeft.x = ~~(topLeft.x + pixelSnapEpsilon); - topLeft.y = ~~(topLeft.y + pixelSnapEpsilon); + topLeft.x = ~~(topLeft.x + sign(topLeft.x) * pixelSnapEpsilon); + topLeft.y = ~~(topLeft.y + sign(topLeft.y) * pixelSnapEpsilon); - topRight.x = ~~(topRight.x + pixelSnapEpsilon); - topRight.y = ~~(topRight.y + pixelSnapEpsilon); + topRight.x = ~~(topRight.x + sign(topRight.x) * pixelSnapEpsilon); + topRight.y = ~~(topRight.y + sign(topRight.y) * pixelSnapEpsilon); - bottomLeft.x = ~~(bottomLeft.x + pixelSnapEpsilon); - bottomLeft.y = ~~(bottomLeft.y + pixelSnapEpsilon); + bottomLeft.x = ~~(bottomLeft.x + sign(bottomLeft.x) * pixelSnapEpsilon); + bottomLeft.y = ~~(bottomLeft.y + sign(bottomLeft.y) * pixelSnapEpsilon); - bottomRight.x = ~~(bottomRight.x + pixelSnapEpsilon); - bottomRight.y = ~~(bottomRight.y + pixelSnapEpsilon); + bottomRight.x = ~~(bottomRight.x + sign(bottomRight.x) * pixelSnapEpsilon); + bottomRight.y = ~~(bottomRight.y + sign(bottomRight.y) * pixelSnapEpsilon); } const tint = this._context.tint; @@ -208,10 +224,13 @@ export class ImageRenderer implements RendererPlugin { const imageWidth = image.width || width; const imageHeight = image.height || height; - const uvx0 = (sx) / imageWidth; - const uvy0 = (sy) / imageHeight; - const uvx1 = (sx + sw - 0.01) / imageWidth; - const uvy1 = (sy + sh - 0.01) / imageHeight; + 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 = image.width; + const txHeight = image.height; // update data const vertexBuffer = this._layout.vertexBuffer.bufferData; @@ -220,6 +239,8 @@ export class ImageRenderer implements RendererPlugin { vertexBuffer[this._vertexIndex++] = topLeft.x; vertexBuffer[this._vertexIndex++] = topLeft.y; vertexBuffer[this._vertexIndex++] = opacity; + vertexBuffer[this._vertexIndex++] = txWidth; + vertexBuffer[this._vertexIndex++] = txHeight; vertexBuffer[this._vertexIndex++] = uvx0; vertexBuffer[this._vertexIndex++] = uvy0; vertexBuffer[this._vertexIndex++] = textureId; @@ -232,6 +253,8 @@ export class ImageRenderer implements RendererPlugin { vertexBuffer[this._vertexIndex++] = bottomLeft.x; vertexBuffer[this._vertexIndex++] = bottomLeft.y; vertexBuffer[this._vertexIndex++] = opacity; + vertexBuffer[this._vertexIndex++] = txWidth; + vertexBuffer[this._vertexIndex++] = txHeight; vertexBuffer[this._vertexIndex++] = uvx0; vertexBuffer[this._vertexIndex++] = uvy1; vertexBuffer[this._vertexIndex++] = textureId; @@ -244,6 +267,8 @@ export class ImageRenderer implements RendererPlugin { vertexBuffer[this._vertexIndex++] = topRight.x; vertexBuffer[this._vertexIndex++] = topRight.y; vertexBuffer[this._vertexIndex++] = opacity; + vertexBuffer[this._vertexIndex++] = txWidth; + vertexBuffer[this._vertexIndex++] = txHeight; vertexBuffer[this._vertexIndex++] = uvx1; vertexBuffer[this._vertexIndex++] = uvy0; vertexBuffer[this._vertexIndex++] = textureId; @@ -256,6 +281,8 @@ export class ImageRenderer implements RendererPlugin { vertexBuffer[this._vertexIndex++] = bottomRight.x; vertexBuffer[this._vertexIndex++] = bottomRight.y; vertexBuffer[this._vertexIndex++] = opacity; + vertexBuffer[this._vertexIndex++] = txWidth; + vertexBuffer[this._vertexIndex++] = txHeight; vertexBuffer[this._vertexIndex++] = uvx1; vertexBuffer[this._vertexIndex++] = uvy1; vertexBuffer[this._vertexIndex++] = textureId; @@ -280,11 +307,14 @@ export class ImageRenderer implements RendererPlugin { this._shader.use(); // Bind the memory layout and upload data - this._layout.use(true, 4 * 10 * this._imageCount); + this._layout.use(true, 4 * 12 * this._imageCount); // 4 verts * 12 components // 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); diff --git a/src/engine/Graphics/Context/image-renderer/image-renderer.vert.glsl b/src/engine/Graphics/Context/image-renderer/image-renderer.vert.glsl index 43100264c..c9f0fc4d9 100644 --- a/src/engine/Graphics/Context/image-renderer/image-renderer.vert.glsl +++ b/src/engine/Graphics/Context/image-renderer/image-renderer.vert.glsl @@ -1,7 +1,7 @@ #version 300 es in vec2 a_position; -// Opacity +// Opacity in float a_opacity; out float v_opacity; @@ -9,6 +9,10 @@ out float v_opacity; in vec2 a_texcoord; out vec2 v_texcoord; +// Texture res +in vec2 a_res; +out vec2 v_res; + // Texture number in lowp float a_textureIndex; out lowp float v_textureIndex; @@ -18,16 +22,19 @@ out vec4 v_tint; uniform mat4 u_matrix; -void main() { - // Set the vertex position using the ortho transform matrix - gl_Position = u_matrix * vec4(a_position, 0.0, 1.0); - - // Pass through the Opacity to the fragment shader - v_opacity = a_opacity; - // Pass through the UV coord to the fragment shader - v_texcoord = a_texcoord; - // Pass through the texture number to the fragment shader - v_textureIndex = a_textureIndex; - // Pass through the tint - v_tint = a_tint; +void main(){ + // Set the vertex position using the ortho transform matrix + gl_Position=u_matrix*vec4(a_position,0.,1.); + + // Pass through the Opacity to the fragment shader + v_opacity=a_opacity; + // Pass through the UV coord to the fragment shader + v_texcoord=a_texcoord; + + v_res = a_res; + + // Pass through the texture number to the fragment shader + v_textureIndex=a_textureIndex; + // Pass through the tint + v_tint=a_tint; } \ No newline at end of file diff --git a/src/engine/Graphics/Context/render-target.ts b/src/engine/Graphics/Context/render-target.ts index f05704e2e..0c2d14634 100644 --- a/src/engine/Graphics/Context/render-target.ts +++ b/src/engine/Graphics/Context/render-target.ts @@ -1,13 +1,65 @@ import { RenderSource } from './render-source'; +declare global { + interface WebGL2RenderingContext { + /** + * Experimental only in chrome + */ + drawingBufferFormat?: number; + } +} + + +export interface RenderTargetOptions { + gl: WebGL2RenderingContext; + width: number; + height: number; + transparency: boolean; + /** + * Optionally enable render buffer multisample anti-aliasing + * + * By default false + */ + antialias?: boolean; + /** + * Optionally specify number of anti-aliasing samples to use + * + * By default the max for the platform is used if antialias is on. + */ + samples?: number; +} + export class RenderTarget { width: number; height: number; - private _gl: WebGLRenderingContext; - constructor(options: {gl: WebGLRenderingContext, width: number, height: number}) { + transparency: boolean; + antialias: boolean = false; + samples: number = 1; + private _gl: WebGL2RenderingContext; + public readonly bufferFormat: number; + constructor(options: RenderTargetOptions) { + this._gl = options.gl; this.width = options.width; this.height = options.height; - this._gl = options.gl; + this.transparency = options.transparency; + this.antialias = options.antialias ?? this.antialias; + this.samples = options.samples ?? this._gl.getParameter(this._gl.MAX_SAMPLES); + + const gl = this._gl; + // Determine current context format for blitting later needs to match + if (gl.drawingBufferFormat) { + this.bufferFormat = gl.drawingBufferFormat; + } else { + // Documented in webgl spec + // https://registry.khronos.org/webgl/specs/latest/1.0/ + if (this.transparency) { + this.bufferFormat = gl.RGBA8; + } else { + this.bufferFormat = gl.RGB8; + } + } + + this._setupRenderBuffer(); this._setupFramebuffer(); } @@ -15,8 +67,30 @@ export class RenderTarget { const gl = this._gl; this.width = width; this.height = height; + + // update backing texture size gl.bindTexture(gl.TEXTURE_2D, this._frameTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + // update render buffer size + if (this._renderBuffer) { + gl.bindRenderbuffer(gl.RENDERBUFFER, this._renderBuffer); + gl.renderbufferStorageMultisample( + gl.RENDERBUFFER, + Math.min(this.samples, gl.getParameter(gl.MAX_SAMPLES)), + this.bufferFormat, + this.width, + this.height); + } + } + + private _renderBuffer: WebGLRenderbuffer; + public get renderBuffer() { + return this._renderBuffer; + } + private _renderFrameBuffer: WebGLFramebuffer; + public get renderFrameBuffer() { + return this._renderFrameBuffer; } private _frameBuffer: WebGLFramebuffer; @@ -27,6 +101,25 @@ export class RenderTarget { public get frameTexture() { return this._frameTexture; } + + private _setupRenderBuffer() { + if (this.antialias) { + const gl = this._gl; + // Render buffers can be used as an input to a shader + this._renderBuffer = gl.createRenderbuffer(); + this._renderFrameBuffer = gl.createFramebuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, this._renderBuffer); + gl.renderbufferStorageMultisample( + gl.RENDERBUFFER, + Math.min(this.samples, gl.getParameter(gl.MAX_SAMPLES)), + this.bufferFormat, + this.width, + this.height); + gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderFrameBuffer); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, this._renderBuffer); + } + } + private _setupFramebuffer() { // Allocates frame buffer const gl = this._gl; @@ -48,21 +141,75 @@ export class RenderTarget { this._frameBuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, this._frameTexture, 0); + // Reset after initialized this.disable(); } public toRenderSource() { + if (this.renderBuffer) { + this.blitRenderBufferToFrameBuffer(); + } const source = new RenderSource(this._gl, this._frameTexture); return source; } + public blitToScreen() { + const gl = this._gl; + // set to size of canvas's drawingBuffer + if (this._renderBuffer) { + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.renderFrameBuffer); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 1.0, 1.0]); + gl.blitFramebuffer( + 0, 0, this.width, this.height, + 0, 0, this.width, this.height, + gl.COLOR_BUFFER_BIT, gl.LINEAR); + } else { + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.frameBuffer); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 1.0, 1.0]); + gl.blitFramebuffer( + 0, 0, this.width, this.height, + 0, 0, this.width, this.height, + gl.COLOR_BUFFER_BIT, gl.LINEAR); + } + } + + public blitRenderBufferToFrameBuffer() { + if (this._renderBuffer) { + const gl = this._gl; + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.renderFrameBuffer); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.frameBuffer); + gl.clearBufferfv(gl.COLOR, 0, [0.0, 0.0, 1.0, 1.0]); + gl.blitFramebuffer( + 0, 0, this.width, this.height, + 0, 0, this.width, this.height, + gl.COLOR_BUFFER_BIT, gl.LINEAR); + } + } + + public copyToTexture(texture: WebGLTexture) { + const gl = this._gl; + if (this._renderBuffer) { + this.blitRenderBufferToFrameBuffer(); + } + gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, this.width, this.height, 0); + } + /** * When called, all drawing gets redirected to this render target */ public use() { const gl = this._gl; - gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer); + if (this.antialias) { + gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderFrameBuffer); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer); + } + // very important to set the viewport to the size of the framebuffer texture gl.viewport(0, 0, this.width, this.height); } diff --git a/src/engine/Graphics/Context/texture-loader.ts b/src/engine/Graphics/Context/texture-loader.ts index 4ae7a1344..aef431c4f 100644 --- a/src/engine/Graphics/Context/texture-loader.ts +++ b/src/engine/Graphics/Context/texture-loader.ts @@ -71,16 +71,19 @@ export class TextureLoader { // No texture exists create a new one tex = gl.createTexture(); + // TODO implement texture gc with weakmap and timer TextureLoader.checkImageSizeSupportedAndLog(image); gl.bindTexture(gl.TEXTURE_2D, tex); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + // TODO make configurable gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // NEAREST for pixel art, LINEAR for hi-res const filterMode = filtering ?? TextureLoader.filtering; + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterMode === ImageFiltering.Pixel ? gl.NEAREST : gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterMode === ImageFiltering.Pixel ? gl.NEAREST : gl.LINEAR); diff --git a/src/engine/Graphics/Context/vertex-layout.ts b/src/engine/Graphics/Context/vertex-layout.ts index 9a070d7d8..2ff4c21b1 100644 --- a/src/engine/Graphics/Context/vertex-layout.ts +++ b/src/engine/Graphics/Context/vertex-layout.ts @@ -114,7 +114,7 @@ export class VertexLayout { } if (this._vertexBuffer.bufferData.length % componentsPerVertex !== 0) { - this._logger.warn(`The vertex component size (${componentsPerVertex}) does divide evenly into the specified vertex buffer` + this._logger.warn(`The vertex component size (${componentsPerVertex}) does NOT divide evenly into the specified vertex buffer` +` (${this._vertexBuffer.bufferData.length})`); } } diff --git a/src/engine/Screen.ts b/src/engine/Screen.ts index 89f5bf7c3..f471faa31 100644 --- a/src/engine/Screen.ts +++ b/src/engine/Screen.ts @@ -164,6 +164,11 @@ export interface ScreenOptions { * Optionally set antialiasing, defaults to true. If set to true, images will be smoothed */ antialiasing?: boolean; + + /** + * Optionally set the image rendering CSS hint on the canvas element, default is auto + */ + canvasImageRendering?: 'auto' | 'pixelated'; /** * Optionally override the pixel ratio to use for the screen, otherwise calculated automatically from the browser */ @@ -253,6 +258,7 @@ export class Screen { public events = new EventEmitter(); private _canvas: HTMLCanvasElement; private _antialiasing: boolean = true; + private _canvasImageRendering: 'auto' | 'pixelated' = 'auto'; private _contentResolution: ScreenDimension; private _browser: BrowserEvents; private _camera: Camera; @@ -276,6 +282,7 @@ export class Screen { this._canvas = options.canvas; this.graphicsContext = options.context; this._antialiasing = options.antialiasing ?? this._antialiasing; + this._canvasImageRendering = options.canvasImageRendering ?? this._canvasImageRendering; this._browser = options.browser; this._pixelRatioOverride = options.pixelRatio; @@ -472,7 +479,7 @@ export class Screen { } } - if (this._antialiasing) { + if (this._canvasImageRendering === 'auto') { this._canvas.style.imageRendering = 'auto'; } else { this._canvas.style.imageRendering = 'pixelated'; diff --git a/src/spec/EngineSpec.ts b/src/spec/EngineSpec.ts index cc715aa75..07555c739 100644 --- a/src/spec/EngineSpec.ts +++ b/src/spec/EngineSpec.ts @@ -759,6 +759,131 @@ describe('The engine', () => { // TODO timeout await expectAsync(image).toEqualImage('src/spec/images/EngineSpec/screenshot.png', 1.0); }); + it('can snap to pixel', async () => { + const engine = TestUtils.engine({ + snapToPixel: true, + antialiasing: false, + width: 256, + height: 256 + }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + const playerImage = new ex.ImageSource('src/spec/images/EngineSpec/hero.png'); + const playerSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: playerImage, + grid: { + spriteWidth: 16, + spriteHeight: 16, + rows: 8, + columns: 8 + } + }); + const backgroundImage = new ex.ImageSource('src/spec/images/EngineSpec/tileset.png'); + const backgroundSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: backgroundImage, + grid: { + spriteHeight: 16, + spriteWidth: 16, + columns: 27, + rows: 15 + } + }); + await playerImage.load(); + await backgroundImage.load(); + + + const tilemap = new ex.TileMap({ + tileWidth: 16, + tileHeight: 16, + rows: 20, + columns: 20, + pos: ex.vec(0, 0) + }); + tilemap.tiles.forEach(t => { + t.addGraphic(backgroundSpriteSheet.getSprite(6, 0)); + }); + + const player = new ex.Actor({ + anchor: ex.vec(0, 0), + pos: ex.vec(16 * 8 + .9, 16 * 8 + .9) + }); + player.graphics.use(playerSpriteSheet.getSprite(0, 0)); + + engine.add(tilemap); + engine.add(player); + engine.currentScene.camera.pos = player.pos.add(ex.vec(.05, .05)); + + clock.step(); + + await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('src/spec/images/EngineSpec/snaptopixel.png'); + }); + + it('can do subpixel AA on pixel art', async () => { + const engine = TestUtils.engine({ + pixelArt: true, + antialiasing: { + nativeContextAntialiasing: false, + multiSampleAntialiasing: true, + pixelArtSampler: true, + canvasImageRendering: 'auto', + filtering: ex.ImageFiltering.Blended + }, + width: 256, + height: 256, + suppressHiDPIScaling: false, + pixelRatio: 2 + }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + const playerImage = new ex.ImageSource('src/spec/images/EngineSpec/hero.png'); + const playerSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: playerImage, + grid: { + spriteWidth: 16, + spriteHeight: 16, + rows: 8, + columns: 8 + } + }); + const backgroundImage = new ex.ImageSource('src/spec/images/EngineSpec/tileset.png'); + const backgroundSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: backgroundImage, + grid: { + spriteHeight: 16, + spriteWidth: 16, + columns: 27, + rows: 15 + } + }); + await playerImage.load(); + await backgroundImage.load(); + + + const tilemap = new ex.TileMap({ + tileWidth: 16, + tileHeight: 16, + rows: 20, + columns: 20, + pos: ex.vec(0, 0) + }); + tilemap.tiles.forEach(t => { + t.addGraphic(backgroundSpriteSheet.getSprite(6, 0)); + }); + + const player = new ex.Actor({ + anchor: ex.vec(0, 0), + pos: ex.vec(127.45599999999973, 117.3119999999999) + }); + player.graphics.use(playerSpriteSheet.getSprite(0, 0)); + + engine.add(tilemap); + engine.add(player); + engine.currentScene.camera.pos = player.pos; + + clock.step(); + await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('src/spec/images/EngineSpec/pixelart.png'); + }); + describe('lifecycle overrides', () => { let engine: ex.Engine; beforeEach(() => { diff --git a/src/spec/ExcaliburGraphicsContextSpec.ts b/src/spec/ExcaliburGraphicsContextSpec.ts index eff5a83a4..6ece4d247 100644 --- a/src/spec/ExcaliburGraphicsContextSpec.ts +++ b/src/spec/ExcaliburGraphicsContextSpec.ts @@ -288,7 +288,8 @@ describe('The ExcaliburGraphicsContext', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, + multiSampleAntialiasing: false, snapToPixel: true }); const rect = new ex.Rectangle({ @@ -311,7 +312,8 @@ describe('The ExcaliburGraphicsContext', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, + multiSampleAntialiasing: false, snapToPixel: true }); const rect = new ex.Rectangle({ @@ -336,7 +338,8 @@ describe('The ExcaliburGraphicsContext', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, + multiSampleAntialiasing: false, snapToPixel: true }); @@ -356,7 +359,8 @@ describe('The ExcaliburGraphicsContext', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, + multiSampleAntialiasing: false, snapToPixel: true }); @@ -376,7 +380,8 @@ describe('The ExcaliburGraphicsContext', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, + multiSampleAntialiasing: false, snapToPixel: true }); @@ -396,7 +401,8 @@ describe('The ExcaliburGraphicsContext', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, + multiSampleAntialiasing: false, snapToPixel: true }); @@ -430,11 +436,6 @@ describe('The ExcaliburGraphicsContext', () => { sut.clear(); sut.drawImage(rect._bitmap, 20, 20); - // sut.opacity = .5; - // sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Green); - // sut.drawLine(ex.vec(10, 10), ex.vec(90, 90), ex.Color.Red, 5); - // sut.drawLine(ex.vec(90, 10), ex.vec(10, 90), ex.Color.Red, 5); - // sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); sut.flush(); await expectAsync(flushWebGLCanvasTo2D(canvasElement)).toEqualImage( @@ -510,6 +511,8 @@ describe('The ExcaliburGraphicsContext', () => { const sut = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvasElement, enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, backgroundColor: ex.Color.White, snapToPixel: false }); @@ -524,6 +527,8 @@ describe('The ExcaliburGraphicsContext', () => { 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); @@ -560,6 +565,8 @@ describe('The ExcaliburGraphicsContext', () => { const sut = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvasElement, enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, backgroundColor: ex.Color.White, snapToPixel: false }); @@ -574,6 +581,7 @@ describe('The ExcaliburGraphicsContext', () => { 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); @@ -608,6 +616,8 @@ describe('The ExcaliburGraphicsContext', () => { const sut = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvasElement, enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, backgroundColor: ex.Color.White }); @@ -629,6 +639,8 @@ describe('The ExcaliburGraphicsContext', () => { const sut = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvasElement, enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, backgroundColor: ex.Color.White }); @@ -881,6 +893,8 @@ describe('The ExcaliburGraphicsContext', () => { const sut = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvasElement, enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, backgroundColor: ex.Color.White, snapToPixel: false }); @@ -899,6 +913,8 @@ describe('The ExcaliburGraphicsContext', () => { const sut = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvasElement, enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, backgroundColor: ex.Color.White }); diff --git a/src/spec/ImageSourceSpec.ts b/src/spec/ImageSourceSpec.ts index a453fd9bd..c0caea5da 100644 --- a/src/spec/ImageSourceSpec.ts +++ b/src/spec/ImageSourceSpec.ts @@ -86,7 +86,7 @@ describe('A ImageSource', () => { const webgl = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas }); - const imageRenderer = new ImageRenderer(); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); imageRenderer.initialize(webgl.__gl, webgl); spyOn(webgl.textureLoader, 'load').and.callThrough(); @@ -107,7 +107,7 @@ describe('A ImageSource', () => { const webgl = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas }); - const imageRenderer = new ImageRenderer(); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); imageRenderer.initialize(webgl.__gl, webgl); spyOn(webgl.textureLoader, 'load').and.callThrough(); @@ -145,7 +145,7 @@ describe('A ImageSource', () => { expect(spriteFontImage.image.src).toBe(''); }); - it('will resolve the image if alreadly loaded', async () => { + it('will resolve the image if already loaded', async () => { const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png'); const image = await spriteFontImage.load(); diff --git a/src/spec/ScreenSpec.ts b/src/spec/ScreenSpec.ts index 6dfe8bd83..bebacad16 100644 --- a/src/spec/ScreenSpec.ts +++ b/src/spec/ScreenSpec.ts @@ -558,6 +558,7 @@ describe('A Screen', () => { const sut = new ex.Screen({ canvas, context, + canvasImageRendering: 'pixelated', browser, viewport: { width: 800, height: 600 }, pixelRatio: 2, @@ -596,6 +597,7 @@ describe('A Screen', () => { canvas: canvasStub, context, browser, + canvasImageRendering: 'pixelated', viewport: { width: 800, height: 600 }, pixelRatio: 2, antialiasing: false diff --git a/src/spec/SpriteSpec.ts b/src/spec/SpriteSpec.ts index 64757f957..b92099dfa 100644 --- a/src/spec/SpriteSpec.ts +++ b/src/spec/SpriteSpec.ts @@ -16,7 +16,13 @@ describe('A Sprite Graphic', () => { canvasElement = document.createElement('canvas'); canvasElement.width = 100; canvasElement.height = 100; - ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement, smoothing: false, snapToPixel: false }); + ctx = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement, + uvPadding: 0.01, + antialiasing: false, + snapToPixel: false, + pixelArtSampler: false + }); }); it('exists', () => { diff --git a/src/spec/images/EngineSpec/hero.png b/src/spec/images/EngineSpec/hero.png new file mode 100644 index 000000000..55b0fdd42 Binary files /dev/null and b/src/spec/images/EngineSpec/hero.png differ diff --git a/src/spec/images/EngineSpec/pixelart.png b/src/spec/images/EngineSpec/pixelart.png new file mode 100644 index 000000000..646d8a50a Binary files /dev/null and b/src/spec/images/EngineSpec/pixelart.png differ diff --git a/src/spec/images/EngineSpec/snaptopixel.png b/src/spec/images/EngineSpec/snaptopixel.png new file mode 100644 index 000000000..1ce2b636a Binary files /dev/null and b/src/spec/images/EngineSpec/snaptopixel.png differ diff --git a/src/spec/images/EngineSpec/tileset.png b/src/spec/images/EngineSpec/tileset.png new file mode 100644 index 000000000..d671166a0 Binary files /dev/null and b/src/spec/images/EngineSpec/tileset.png differ diff --git a/src/spec/images/GraphicsSystemSpec/graphics-system.png b/src/spec/images/GraphicsSystemSpec/graphics-system.png index 7358cb276..72fe3ccda 100644 Binary files a/src/spec/images/GraphicsSystemSpec/graphics-system.png and b/src/spec/images/GraphicsSystemSpec/graphics-system.png differ diff --git a/src/spec/images/GraphicsTextSpec/long-text-linux.png b/src/spec/images/GraphicsTextSpec/long-text-linux.png index f47be341e..6150663a3 100644 Binary files a/src/spec/images/GraphicsTextSpec/long-text-linux.png and b/src/spec/images/GraphicsTextSpec/long-text-linux.png differ diff --git a/src/spec/images/GraphicsTextSpec/long-text.png b/src/spec/images/GraphicsTextSpec/long-text.png index 6ed04f2ba..a14e87e86 100644 Binary files a/src/spec/images/GraphicsTextSpec/long-text.png and b/src/spec/images/GraphicsTextSpec/long-text.png differ diff --git a/src/spec/util/TestUtils.ts b/src/spec/util/TestUtils.ts index 48e123401..084b2fa21 100644 --- a/src/spec/util/TestUtils.ts +++ b/src/spec/util/TestUtils.ts @@ -16,6 +16,7 @@ export namespace TestUtils { suppressHiDPIScaling: true, suppressPlayButton: true, snapToPixel: false, + antialiasing: false, displayMode: ex.DisplayMode.Fixed, ...options };