From 470439f0be2eca9ea18465b6853bc2cefca65e6c Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 6 Apr 2024 22:18:36 -0500 Subject: [PATCH] feat: Implement image wrapping configuration (#2963) This PR allows users to configure the ImageSource wrapping mode: Clamp, Repeat, or Mirror. Example of using the Repeat mode to repeat noise textures over time https://github.com/excaliburjs/Excalibur/assets/612071/ca199c17-569e-4f38-9810-72227e2ebb52 --- CHANGELOG.md | 10 ++ sandbox/tests/imagewrapping/index.html | 13 ++ sandbox/tests/imagewrapping/index.ts | 59 +++++++++ sandbox/tests/imagewrapping/noise.png | Bin 0 -> 8068 bytes .../Context/image-renderer/image-renderer.ts | 22 ++-- .../material-renderer/material-renderer.ts | 23 ++-- src/engine/Graphics/Context/material.ts | 23 ++-- src/engine/Graphics/Context/texture-loader.ts | 54 +++++++- src/engine/Graphics/Filtering.ts | 11 ++ src/engine/Graphics/FontTextInstance.ts | 4 +- src/engine/Graphics/ImageSource.ts | 71 +++++++++- src/engine/Graphics/Wrapping.ts | 24 ++++ src/engine/Graphics/index.ts | 1 + src/spec/ImageSourceSpec.ts | 123 +++++++++++++++++- 14 files changed, 398 insertions(+), 40 deletions(-) create mode 100644 sandbox/tests/imagewrapping/index.html create mode 100644 sandbox/tests/imagewrapping/index.ts create mode 100644 sandbox/tests/imagewrapping/noise.png create mode 100644 src/engine/Graphics/Wrapping.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2227735e1..20b14aa60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/sandbox/tests/imagewrapping/index.html b/sandbox/tests/imagewrapping/index.html new file mode 100644 index 000000000..45b4bf43e --- /dev/null +++ b/sandbox/tests/imagewrapping/index.html @@ -0,0 +1,13 @@ + + + + + + Image Wrapping + + + + + + + \ No newline at end of file diff --git a/sandbox/tests/imagewrapping/index.ts b/sandbox/tests/imagewrapping/index.ts new file mode 100644 index 000000000..d7cbcfe4c --- /dev/null +++ b/sandbox/tests/imagewrapping/index.ts @@ -0,0 +1,59 @@ +/// + +// 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); \ No newline at end of file diff --git a/sandbox/tests/imagewrapping/noise.png b/sandbox/tests/imagewrapping/noise.png new file mode 100644 index 0000000000000000000000000000000000000000..98ab46077a94bd74d1ab03a94ba56023f9a9d8cf GIT binary patch literal 8068 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 Sg)}k%0000 { private _logger = Logger.getInstance(); private _resource: Resource; public filtering: ImageFiltering; + public wrapping: ImageWrapConfiguration; /** * The original size of the source image in pixels @@ -57,15 +71,40 @@ export class ImageSource implements Loadable { */ public ready: Promise = this._readyFuture.promise; + public readonly path: string; + + /** + * The path to the image, can also be a data url like 'data:image/' + * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute + * @param options + */ + constructor(path: string, options?: ImageSourceOptions); /** * The path to the image, can also be a data url like 'data:image/' * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute * @param bustCache {boolean} Should excalibur add a cache busting querystring? * @param filtering {ImageFiltering} Optionally override the image filtering set by [[EngineOptions.antialiasing]] */ - constructor(public readonly path: string, bustCache: boolean = false, filtering?: ImageFiltering) { + constructor(path: string, bustCache: boolean, filtering?: ImageFiltering); + constructor(path: string, bustCacheOrOptions: boolean | ImageSourceOptions, filtering?: ImageFiltering) { + this.path = path; + let bustCache = false; + let wrapping: ImageWrapConfiguration | ImageWrapping; + if (typeof bustCacheOrOptions === 'boolean') { + bustCache = bustCacheOrOptions; + } else { + ({ filtering, wrapping, bustCache } = {...bustCacheOrOptions}); + } this._resource = new Resource(path, 'blob', bustCache); - this.filtering = filtering; + this.filtering = filtering ?? this.filtering; + if (typeof wrapping === 'string') { + this.wrapping = { + x: wrapping, + y: wrapping + }; + } else { + this.wrapping = wrapping ?? this.wrapping; + } if (path.endsWith('.svg') || path.endsWith('.gif')) { this._logger.warn(`Image type is not fully supported, you may have mixed results ${path}. Fully supported: jpg, bmp, and png`); } @@ -82,9 +121,29 @@ export class ImageSource implements Loadable { imageSource.data.setAttribute('data-original-src', 'image-element'); if (options?.filtering) { - imageSource.data.setAttribute('filtering', options?.filtering); + imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, options?.filtering); + } else { + imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, ImageFiltering.Blended); + } + + if (options?.wrapping) { + let wrapping: ImageWrapConfiguration; + if (typeof options.wrapping === 'string') { + wrapping = { + x: options.wrapping, + y: options.wrapping + }; + } else { + wrapping = { + x: options.wrapping.x, + y: options.wrapping.y + }; + } + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, wrapping.x); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, wrapping.y); } else { - imageSource.data.setAttribute('filtering', ImageFiltering.Blended); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, ImageWrapping.Clamp); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, ImageWrapping.Clamp); } TextureLoader.checkImageSizeSupportedAndLog(image); @@ -145,7 +204,9 @@ export class ImageSource implements Loadable { throw `Error loading ImageSource from path '${this.path}' with error [${error.message}]`; } // Do a bad thing to pass the filtering as an attribute - this.data.setAttribute('filtering', this.filtering); + this.data.setAttribute(ImageSourceAttributeConstants.Filtering, this.filtering); + this.data.setAttribute(ImageSourceAttributeConstants.WrappingX, this.wrapping?.x ?? ImageWrapping.Clamp); + this.data.setAttribute(ImageSourceAttributeConstants.WrappingY, this.wrapping?.y ?? ImageWrapping.Clamp); // todo emit complete this._readyFuture.resolve(this.data); diff --git a/src/engine/Graphics/Wrapping.ts b/src/engine/Graphics/Wrapping.ts new file mode 100644 index 000000000..324c21a31 --- /dev/null +++ b/src/engine/Graphics/Wrapping.ts @@ -0,0 +1,24 @@ + +/** + * Describes the different image wrapping modes + */ +export enum ImageWrapping { + + Clamp = 'Clamp', + + Repeat = 'Repeat', + + Mirror = 'Mirror' +} + +/** + * + */ +export function parseImageWrapping(val: string): ImageWrapping { + switch (val) { + case ImageWrapping.Clamp: return ImageWrapping.Clamp; + case ImageWrapping.Repeat: return ImageWrapping.Repeat; + case ImageWrapping.Mirror: return ImageWrapping.Mirror; + default: return ImageWrapping.Clamp; + } +} \ No newline at end of file diff --git a/src/engine/Graphics/index.ts b/src/engine/Graphics/index.ts index 2a23e1fa0..b3707a31c 100644 --- a/src/engine/Graphics/index.ts +++ b/src/engine/Graphics/index.ts @@ -40,6 +40,7 @@ export * from './PostProcessor/ColorBlindnessPostProcessor'; export * from './Context/texture-loader'; export * from './Filtering'; +export * from './Wrapping'; // Rendering diff --git a/src/spec/ImageSourceSpec.ts b/src/spec/ImageSourceSpec.ts index c0caea5da..000edaace 100644 --- a/src/spec/ImageSourceSpec.ts +++ b/src/spec/ImageSourceSpec.ts @@ -99,7 +99,15 @@ describe('A ImageSource', () => { expect(image.src).not.toBeNull(); expect(whenLoaded).toHaveBeenCalledTimes(1); - expect(webgl.textureLoader.load).toHaveBeenCalledWith(image, ex.ImageFiltering.Blended, false); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Blended, + wrapping: { + x: ex.ImageWrapping.Clamp, + y: ex.ImageWrapping.Clamp + } + }, false); }); it('can load images with an image filtering Pixel', async () => { @@ -120,7 +128,118 @@ describe('A ImageSource', () => { expect(image.src).not.toBeNull(); expect(whenLoaded).toHaveBeenCalledTimes(1); - expect(webgl.textureLoader.load).toHaveBeenCalledWith(image, ex.ImageFiltering.Pixel, false); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Clamp, + y: ex.ImageWrapping.Clamp + } + }, false); + }); + + it('can load images with an image wrap repeat', async () => { + const canvas = document.createElement('canvas'); + const webgl = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas + }); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); + imageRenderer.initialize(webgl.__gl, webgl); + spyOn(webgl.textureLoader, 'load').and.callThrough(); + + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ + filtering: ex.ImageFiltering.Pixel, + wrapping: ex.ImageWrapping.Repeat + }); + const whenLoaded = jasmine.createSpy('whenLoaded'); + const image = await spriteFontImage.load(); + await spriteFontImage.ready.then(whenLoaded); + + imageRenderer.draw(image, 0, 0); + + expect(image.src).not.toBeNull(); + expect(whenLoaded).toHaveBeenCalledTimes(1); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Repeat, + y: ex.ImageWrapping.Repeat + } + }, false); + }); + + it('can load images with an image wrap repeat', async () => { + const canvas = document.createElement('canvas'); + const webgl = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas + }); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); + imageRenderer.initialize(webgl.__gl, webgl); + spyOn(webgl.textureLoader, 'load').and.callThrough(); + + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ + filtering: ex.ImageFiltering.Pixel, + wrapping: ex.ImageWrapping.Mirror + }); + const whenLoaded = jasmine.createSpy('whenLoaded'); + const image = await spriteFontImage.load(); + await spriteFontImage.ready.then(whenLoaded); + + imageRenderer.draw(image, 0, 0); + + expect(image.src).not.toBeNull(); + expect(whenLoaded).toHaveBeenCalledTimes(1); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Mirror, + y: ex.ImageWrapping.Mirror + } + }, false); + }); + + it('can load images with an image wrap mixed', async () => { + const canvas = document.createElement('canvas'); + const webgl = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas + }); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); + imageRenderer.initialize(webgl.__gl, webgl); + spyOn(webgl.textureLoader, 'load').and.callThrough(); + const texParameteri = spyOn(webgl.__gl, 'texParameteri').and.callThrough(); + const gl = webgl.__gl; + + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Mirror, + y: ex.ImageWrapping.Clamp + } + }); + const whenLoaded = jasmine.createSpy('whenLoaded'); + const image = await spriteFontImage.load(); + await spriteFontImage.ready.then(whenLoaded); + + imageRenderer.draw(image, 0, 0); + + expect(image.src).not.toBeNull(); + expect(whenLoaded).toHaveBeenCalledTimes(1); + expect(texParameteri.calls.argsFor(0)).toEqual([gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT]); + expect(texParameteri.calls.argsFor(1)).toEqual([gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE]); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Mirror, + y: ex.ImageWrapping.Clamp + } + }, false); }); it('can convert to a Sprite', async () => {