Skip to content

Commit

Permalink
feat: Pixel Art Sampler & Antialiasing Improvements (#2739)
Browse files Browse the repository at this point in the history
This PR adds a new pixel art setting and special sampler for sub-pixel pixel art rendering.

Additionally this PR adds MSAA to the framebuffer implementation, giving a nicer look to some graphics when drawing to the quad boundary.

Also fixes an issue with `snapToPixel` where camera did not correctly snap to pixel

Example using the old `antialiasing: false` on pixel art

https://github.com/excaliburjs/Excalibur/assets/612071/c55a09b0-cf7e-409c-a82c-447e0bec198a

New `pixelArt: true`

https://github.com/excaliburjs/Excalibur/assets/612071/eff9f9f3-a731-482a-8e15-e778e60c225e
  • Loading branch information
eonarheim authored Feb 3, 2024
1 parent 84cff95 commit b95b4c5
Show file tree
Hide file tree
Showing 31 changed files with 794 additions and 104 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sandbox/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
12 changes: 12 additions & 0 deletions sandbox/tests/graphicscontext/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Graphics Context</title>
</head>
<body>
<script src="../../lib/excalibur.js"></script>
<script src="./index.js"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions sandbox/tests/graphicscontext/index.ts
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 1 addition & 1 deletion sandbox/tests/raycast/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion sandbox/tests/sprite/sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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;
12 changes: 11 additions & 1 deletion src/engine/Camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
}

Expand Down
112 changes: 103 additions & 9 deletions src/engine/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -74,6 +82,8 @@ export const EngineEvents = {
PostDraw: 'postdraw'
} as const;



/**
* Enum representing the different mousewheel event bubble prevention
*/
Expand Down Expand Up @@ -122,12 +132,49 @@ export interface EngineOptions<TKnownScenes extends string = any> {
* 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
Expand Down Expand Up @@ -601,6 +648,9 @@ export class Engine<TKnownScenes extends string = any> implements CanInitialize,
canvasElementId: '',
canvasElement: undefined,
snapToPixel: false,
antialiasing: true,
pixelArt: false,
powerPreference: 'high-performance',
pointerScope: PointerScope.Canvas,
suppressConsoleBootMessage: null,
suppressMinimumBrowserFeatureDetection: null,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1189,13 +1281,15 @@ 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;
}

/**
* 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;
Expand Down
Loading

0 comments on commit b95b4c5

Please sign in to comment.