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
};