Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement image wrapping configuration #2963

Merged
merged 7 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Added ability to configure image wrapping on `ex.ImageSource` with the new `ex.ImageWrapping.Clamp` (default), `ex.ImageWrapping.Repeat`, and `ex.ImageWrapping.Mirror`.
```typescript
const image = new ex.ImageSource('path/to/image.png', {
filtering: ex.ImageFiltering.Pixel,
wrapping: {
x: ex.ImageWrapping.Repeat,
y: ex.ImageWrapping.Repeat,
}
});
```
- Added pointer event support to `ex.TileMap`'s and individual `ex.Tile`'s
- Added pointer event support to `ex.IsometricMap`'s and individual `ex.IsometricTile`'s
- Added `useAnchor` parameter to `ex.GraphicsGroup` to allow users to opt out of anchor based positioning, if set to false all graphics members
Expand Down
13 changes: 13 additions & 0 deletions sandbox/tests/imagewrapping/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Wrapping</title>
</head>
<body>
<canvas id="game"></canvas>
<script src="../../lib/excalibur.js"></script>
<script src="index.js"></script>
</body>
</html>
59 changes: 59 additions & 0 deletions sandbox/tests/imagewrapping/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/// <reference path="../../lib/excalibur.d.ts" />

// identity tagged template literal lights up glsl-literal vscode plugin
var glsl = x => x[0];
var game = new ex.Engine({
canvasElementId: 'game',
width: 800,
height: 800
});

var fireShader = glsl`#version 300 es
precision mediump float;
uniform float animation_speed;
uniform float offset;
uniform float u_time_ms;
uniform sampler2D u_graphic;
uniform sampler2D noise;
in vec2 v_uv;
out vec4 fragColor;

void main() {
vec2 animatedUV = vec2(v_uv.x, v_uv.y + (u_time_ms / 1000.) * 0.5);
vec4 color = texture(noise, animatedUV);
color.rgb += (v_uv.y - 0.5);
color.rgb = step(color.rgb, vec3(0.5));
color.rgb = vec3(1.0) - color.rgb;

fragColor.rgb = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), v_uv.y);
fragColor.a = color.r;
fragColor.rgb = fragColor.rgb * fragColor.a;
}
`

var noiseImage = new ex.ImageSource('./noise.png', {
filtering: ex.ImageFiltering.Blended,
wrapping: ex.ImageWrapping.Repeat
});

var material = game.graphicsContext.createMaterial({
name: 'fire',
fragmentSource: fireShader,
images: {
'noise': noiseImage
}
})

var actor = new ex.Actor({
pos: ex.vec(0, 200),
anchor: ex.vec(0, 0),
width: 800,
height: 600,
color: ex.Color.Red
});
actor.graphics.material = material;
game.add(actor);

var loader = new ex.Loader([noiseImage]);

game.start(loader);
Binary file added sandbox/tests/imagewrapping/noise.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 14 additions & 8 deletions src/engine/Graphics/Context/image-renderer/image-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { sign } from '../../../Math/util';
import { ImageFiltering } from '../../Filtering';
import { parseImageFiltering } from '../../Filtering';
import { GraphicsDiagnostics } from '../../GraphicsDiagnostics';
import { ImageSourceAttributeConstants } from '../../ImageSource';
import { parseImageWrapping } from '../../Wrapping';
import { HTMLImageSource } from '../ExcaliburGraphicsContext';
import { ExcaliburGraphicsContextWebGL, pixelSnapEpsilon } from '../ExcaliburGraphicsContextWebGL';
import { QuadIndexBuffer } from '../quad-index-buffer';
Expand Down Expand Up @@ -124,15 +126,19 @@ export class ImageRenderer implements RendererPlugin {
if (this._images.has(image)) {
return;
}
const maybeFiltering = image.getAttribute('filtering');
let filtering: ImageFiltering = null;
if (maybeFiltering === ImageFiltering.Blended ||
maybeFiltering === ImageFiltering.Pixel) {
filtering = maybeFiltering;
}
const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering);
const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null;
const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX));
const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY));

const force = image.getAttribute('forceUpload') === 'true' ? true : false;
const texture = this._context.textureLoader.load(image, filtering, force);
const texture = this._context.textureLoader.load(
image,
{
filtering,
wrapping: { x: wrapX, y: wrapY }
},
force);
// remove force attribute after upload
image.removeAttribute('forceUpload');
if (this._textures.indexOf(texture) === -1) {
Expand Down
23 changes: 14 additions & 9 deletions src/engine/Graphics/Context/material-renderer/material-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { vec } from '../../../Math/vector';
import { ImageFiltering } from '../../Filtering';
import { parseImageFiltering } from '../../Filtering';
import { GraphicsDiagnostics } from '../../GraphicsDiagnostics';
import { ImageSourceAttributeConstants } from '../../ImageSource';
import { parseImageWrapping } from '../../Wrapping';
import { HTMLImageSource } from '../ExcaliburGraphicsContext';
import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL';
import { QuadIndexBuffer } from '../quad-index-buffer';
Expand Down Expand Up @@ -204,15 +206,19 @@ export class MaterialRenderer implements RendererPlugin {
}

private _addImageAsTexture(image: HTMLImageSource) {
const maybeFiltering = image.getAttribute('filtering');
let filtering: ImageFiltering = null;
if (maybeFiltering === ImageFiltering.Blended ||
maybeFiltering === ImageFiltering.Pixel) {
filtering = maybeFiltering;
}
const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering);
const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null;
const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX));
const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY));

const force = image.getAttribute('forceUpload') === 'true' ? true : false;
const texture = this._context.textureLoader.load(image, filtering, force);
const texture = this._context.textureLoader.load(
image,
{
filtering,
wrapping: { x: wrapX, y: wrapY }
},
force);
// remove force attribute after upload
image.removeAttribute('forceUpload');
if (this._textures.indexOf(texture) === -1) {
Expand All @@ -228,5 +234,4 @@ export class MaterialRenderer implements RendererPlugin {
flush(): void {
// flush does not do anything, material renderer renders immediately per draw
}

}
23 changes: 14 additions & 9 deletions src/engine/Graphics/Context/material.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { ExcaliburGraphicsContext } from './ExcaliburGraphicsContext';
import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL';
import { Shader } from './shader';
import { Logger } from '../../Util/Log';
import { ImageSource } from '../ImageSource';
import { ImageFiltering } from '../Filtering';
import { ImageSource, ImageSourceAttributeConstants } from '../ImageSource';
import { ImageFiltering, parseImageFiltering } from '../Filtering';
import { parseImageWrapping } from '../Wrapping';

export interface MaterialOptions {
/**
Expand Down Expand Up @@ -191,15 +192,19 @@ export class Material {

private _loadImageSource(image: ImageSource) {
const imageElement = image.image;
const maybeFiltering = imageElement.getAttribute('filtering');
let filtering: ImageFiltering = null;
if (maybeFiltering === ImageFiltering.Blended ||
maybeFiltering === ImageFiltering.Pixel) {
filtering = maybeFiltering;
}
const maybeFiltering = imageElement.getAttribute(ImageSourceAttributeConstants.Filtering);
const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null;
const wrapX = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingX));
const wrapY = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingY));

const force = imageElement.getAttribute('forceUpload') === 'true' ? true : false;
const texture = this._graphicsContext.textureLoader.load(imageElement, filtering, force);
const texture = this._graphicsContext.textureLoader.load(
imageElement,
{
filtering,
wrapping: { x: wrapX, y: wrapY }
},
force);
// remove force attribute after upload
imageElement.removeAttribute('forceUpload');
if (!this._textures.has(image)) {
Expand Down
54 changes: 49 additions & 5 deletions src/engine/Graphics/Context/texture-loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Logger } from '../../Util/Log';
import { ImageFiltering } from '../Filtering';
import { ImageSourceOptions, ImageWrapConfiguration } from '../ImageSource';
import { ImageWrapping } from '../Wrapping';
import { HTMLImageSource } from './ExcaliburGraphicsContext';

/**
Expand All @@ -25,6 +27,7 @@ export class TextureLoader {
* Sets the default filtering for the Excalibur texture loader, default [[ImageFiltering.Blended]]
*/
public static filtering: ImageFiltering = ImageFiltering.Blended;
public static wrapping: ImageWrapConfiguration = {x: ImageWrapping.Clamp, y: ImageWrapping.Clamp};

private _gl: WebGL2RenderingContext;

Expand All @@ -51,16 +54,18 @@ export class TextureLoader {
/**
* Loads a graphic into webgl and returns it's texture info, a webgl context must be previously registered
* @param image Source graphic
* @param filtering {ImageFiltering} The ImageFiltering mode to apply to the loaded texture
* @param options {ImageSourceOptions} Optionally configure the ImageFiltering and ImageWrapping mode to apply to the loaded texture
* @param forceUpdate Optionally force a texture to be reloaded, useful if the source graphic has changed
*/
public load(image: HTMLImageSource, filtering?: ImageFiltering, forceUpdate = false): WebGLTexture {
public load(image: HTMLImageSource, options?: ImageSourceOptions, forceUpdate = false): WebGLTexture {
// Ignore loading if webgl is not registered
const gl = this._gl;
if (!gl) {
return null;
}

const { filtering, wrapping } = {...options};

let tex: WebGLTexture = null;
// If reuse the texture if it's from the same source
if (this.has(image)) {
Expand All @@ -85,9 +90,48 @@ export class TextureLoader {
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);

let wrappingConfig: ImageWrapConfiguration;
if (wrapping) {
if (typeof wrapping === 'string') {
wrappingConfig = {
x: wrapping,
y: wrapping
};
} else {
wrappingConfig = {
x: wrapping.x,
y: wrapping.y
};
}
}
const { x: xWrap, y: yWrap} = (wrappingConfig ?? TextureLoader.wrapping);
switch (xWrap) {
case ImageWrapping.Clamp:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
break;
case ImageWrapping.Repeat:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
break;
case ImageWrapping.Mirror:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
break;
default:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
}
switch (yWrap) {
case ImageWrapping.Clamp:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
break;
case ImageWrapping.Repeat:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
break;
case ImageWrapping.Mirror:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
break;
default:
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;
Expand Down
11 changes: 11 additions & 0 deletions src/engine/Graphics/Filtering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,15 @@ export enum ImageFiltering {
* Blended is useful when you have high resolution artwork and would like it blended and smoothed
*/
Blended = 'Blended'
}

/**
* Parse the image filtering attribute value, if it doesn't match returns null
*/
export function parseImageFiltering(val: string): ImageFiltering | null {
switch (val) {
case ImageFiltering.Pixel: return ImageFiltering.Pixel;
case ImageFiltering.Blended: return ImageFiltering.Blended;
default: return null;
}
}
4 changes: 2 additions & 2 deletions src/engine/Graphics/FontTextInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class FontTextInstance {
const metrics = this.ctx.measureText(maxWidthLine);
let textHeight = Math.abs(metrics.actualBoundingBoxAscent) + Math.abs(metrics.actualBoundingBoxDescent);

// TODO lineheight makes the text bounds wonky
// TODO line height makes the text bounds wonky
const lineAdjustedHeight = textHeight * lines.length;
textHeight = lineAdjustedHeight;
const bottomBounds = lineAdjustedHeight - Math.abs(metrics.actualBoundingBoxAscent);
Expand Down Expand Up @@ -209,7 +209,7 @@ export class FontTextInstance {

if (ex instanceof ExcaliburGraphicsContextWebGL) {
for (const frag of this._textFragments) {
ex.textureLoader.load(frag.canvas, this.font.filtering, true);
ex.textureLoader.load(frag.canvas, { filtering: this.font.filtering }, true);
}
}
this._lastHashCode = hashCode;
Expand Down
Loading
Loading