diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d8de303..e9882436d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed -- +- Changed a rough edge in the `ex.Material` API, if a material was created with a constructor it was lazily initialized. However this causes confusion because now the two ways of creating a material behave differently (the shader is not available immediately on the lazy version). Now `ex.Material` requires the GL graphics context to make sure it always works the same. diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts index 7ff80f546..7fb2e3d3f 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts @@ -264,7 +264,7 @@ export interface ExcaliburGraphicsContext { * @param options * @returns */ - createMaterial(options: MaterialOptions): Material; + createMaterial(options: Omit): Material; /** * Clears the screen with the current background color diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts index ff582f244..8e361c833 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts @@ -340,7 +340,7 @@ export class ExcaliburGraphicsContext2DCanvas implements ExcaliburGraphicsContex return this._state.current.material; } - public createMaterial(_options: MaterialOptions): Material { + public createMaterial(options: Omit): Material { // pass return null; } diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index 84cd18151..fe48094b4 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -498,9 +498,8 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { * @param options * @returns Material */ - public createMaterial(options: MaterialOptions): Material { - const material = new Material(options); - material.initialize(this.__gl, this); + public createMaterial(options: Omit): Material { + const material = new Material({...options, graphicsContext: this}); return material; } diff --git a/src/engine/Graphics/Context/material-renderer/material-renderer.ts b/src/engine/Graphics/Context/material-renderer/material-renderer.ts index 4c5c71244..bd8b392b2 100644 --- a/src/engine/Graphics/Context/material-renderer/material-renderer.ts +++ b/src/engine/Graphics/Context/material-renderer/material-renderer.ts @@ -58,7 +58,6 @@ export class MaterialRenderer implements RendererPlugin { // Extract context info const material = this._context.material; - material.initialize(gl, this._context); const transform = this._context.getTransform(); const opacity = this._context.opacity; diff --git a/src/engine/Graphics/Context/material.ts b/src/engine/Graphics/Context/material.ts index 1c710c26f..a4744f39a 100644 --- a/src/engine/Graphics/Context/material.ts +++ b/src/engine/Graphics/Context/material.ts @@ -1,6 +1,8 @@ import { Color } from '../../Color'; +import { ExcaliburGraphicsContext } from './ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL'; import { Shader } from './shader'; +import { Logger } from '../../Util/Log'; export interface MaterialOptions { /** @@ -8,6 +10,11 @@ export interface MaterialOptions { */ name?: string; + /** + * Excalibur graphics context to create the material (only WebGL is supported at the moment) + */ + graphicsContext?: ExcaliburGraphicsContext; + /** * Optionally specify a vertex shader * @@ -82,6 +89,7 @@ void main() { `; export class Material { + private _logger = Logger.getInstance(); private _name: string; private _shader: Shader; private _color: Color = Color.Transparent; @@ -89,19 +97,27 @@ export class Material { private _fragmentSource: string; private _vertexSource: string; constructor(options: MaterialOptions) { - const { color, name, vertexSource, fragmentSource } = options; + const { color, name, vertexSource, fragmentSource, graphicsContext } = options; this._name = name; this._vertexSource = vertexSource ?? defaultVertexSource; this._fragmentSource = fragmentSource; this._color = color ?? this._color; + if (!graphicsContext) { + throw Error(`Material ${name} must be provided an excalibur webgl graphics context`); + } + if (graphicsContext instanceof ExcaliburGraphicsContextWebGL) { + this._initialize(graphicsContext); + } else { + this._logger.warn(`Material ${name} was created in 2D Canvas mode, currently only WebGL is supported`); + } } - initialize(_gl: WebGL2RenderingContext, _context: ExcaliburGraphicsContextWebGL) { + private _initialize(graphicsContextWebGL: ExcaliburGraphicsContextWebGL) { if (this._initialized) { return; } - this._shader = _context.createShader({ + this._shader = graphicsContextWebGL.createShader({ vertexSource: this._vertexSource, fragmentSource: this._fragmentSource }); diff --git a/src/spec/MaterialRendererSpec.ts b/src/spec/MaterialRendererSpec.ts index 8efc60a5c..8012b950b 100644 --- a/src/spec/MaterialRendererSpec.ts +++ b/src/spec/MaterialRendererSpec.ts @@ -3,10 +3,16 @@ import { TestUtils } from './util/TestUtils'; import { ExcaliburAsyncMatchers } from 'excalibur-jasmine'; describe('A Material', () => { + let graphicsContext: ex.ExcaliburGraphicsContext; beforeAll(() => { jasmine.addAsyncMatchers(ExcaliburAsyncMatchers); }); + beforeEach(() => { + const engine = TestUtils.engine(); + graphicsContext = engine.graphicsContext; + }); + it('exists', () => { expect(ex.Material).toBeDefined(); }); @@ -14,25 +20,46 @@ describe('A Material', () => { it('can be created with a name', () => { const material = new ex.Material({ name: 'test', - fragmentSource: '' + graphicsContext, + fragmentSource: `#version 300 es + precision mediump float; + out vec4 color; + void main() { + color = vec4(1.0, 0.0, 0.0, 1.0); + }` }); expect(material.name).toBe('test'); }); - it('throws if not initialized', () => { + it('does not throw when use() is called after ctor', () => { const material = new ex.Material({ name: 'test', - fragmentSource: '' + graphicsContext, + fragmentSource: `#version 300 es + precision mediump float; + out vec4 color; + void main() { + color = vec4(1.0, 0.0, 0.0, 1.0); + }` }); - expect(() => material.use()) - .toThrowError('Material test not yet initialized, use the ExcaliburGraphicsContext.createMaterial() to work around this.'); + expect(() => material.use()).not.toThrow(); }); it('can be created with a custom fragment shader', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black, + smoothing: false, + snapToPixel: true + }); const material = new ex.Material({ name: 'test', + graphicsContext, color: ex.Color.Red, fragmentSource: `#version 300 es precision mediump float; @@ -51,27 +78,17 @@ describe('A Material', () => { }` }); - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black, - smoothing: false, - snapToPixel: true - }); - const tex = new ex.ImageSource('src/spec/images/MaterialRendererSpec/sword.png'); await tex.load(); - context.clear(); - context.save(); - context.material = material; - context.drawImage(tex.image, 0, 0); - context.flush(); - context.restore(); + graphicsContext.clear(); + graphicsContext.save(); + graphicsContext.material = material; + graphicsContext.drawImage(tex.image, 0, 0); + graphicsContext.flush(); + graphicsContext.restore(); - expect(context.material).toBe(null); + expect(graphicsContext.material).toBe(null); await expectAsync(TestUtils.flushWebGLCanvasTo2D(canvas)) .toEqualImage('src/spec/images/MaterialRendererSpec/material.png'); }); @@ -169,8 +186,16 @@ describe('A Material', () => { }); it('can be created with a custom fragment shader with the graphics component', async () => { + const engine = TestUtils.engine({ + width: 100, + height: 100, + antialiasing: false, + snapToPixel: true + }); + const graphicsContext = engine.graphicsContext as ex.ExcaliburGraphicsContextWebGL; const material = new ex.Material({ name: 'test', + graphicsContext, color: ex.Color.Red, fragmentSource: `#version 300 es precision mediump float; @@ -189,13 +214,7 @@ describe('A Material', () => { }` }); - const engine = TestUtils.engine({ - width: 100, - height: 100, - antialiasing: false, - snapToPixel: true - }); - const context = engine.graphicsContext as ex.ExcaliburGraphicsContextWebGL; + const tex = new ex.ImageSource('src/spec/images/MaterialRendererSpec/sword.png'); @@ -212,19 +231,27 @@ describe('A Material', () => { actor.graphics.use(tex.toSprite()); actor.graphics.material = material; - context.clear(); + graphicsContext.clear(); engine.currentScene.add(actor); - engine.currentScene.draw(context, 100); - context.flush(); + engine.currentScene.draw(graphicsContext, 100); + graphicsContext.flush(); - expect(context.material).toBe(null); + expect(graphicsContext.material).toBe(null); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) .toEqualImage('src/spec/images/MaterialRendererSpec/material-component.png'); }); it('can be draw multiple materials', async () => { + const engine = TestUtils.engine({ + width: 100, + height: 100, + antialiasing: false, + snapToPixel: true + }); + const graphicsContext = engine.graphicsContext; const material1 = new ex.Material({ name: 'material1', + graphicsContext, color: ex.Color.Red, fragmentSource: `#version 300 es precision mediump float; @@ -237,6 +264,7 @@ describe('A Material', () => { const material2 = new ex.Material({ name: 'material2', + graphicsContext, color: ex.Color.Blue, fragmentSource: `#version 300 es precision mediump float; @@ -247,13 +275,7 @@ describe('A Material', () => { }` }); - const engine = TestUtils.engine({ - width: 100, - height: 100, - antialiasing: false, - snapToPixel: true - }); - const context = engine.graphicsContext as ex.ExcaliburGraphicsContextWebGL; + const tex = new ex.ImageSource('src/spec/images/MaterialRendererSpec/sword.png'); @@ -279,13 +301,13 @@ describe('A Material', () => { actor2.graphics.use(tex.toSprite()); actor2.graphics.material = material2; - context.clear(); + graphicsContext.clear(); engine.currentScene.add(actor1); engine.currentScene.add(actor2); - engine.currentScene.draw(context, 100); - context.flush(); + engine.currentScene.draw(graphicsContext, 100); + graphicsContext.flush(); - expect(context.material).toBe(null); + expect(graphicsContext.material).toBe(null); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) .toEqualImage('src/spec/images/MaterialRendererSpec/multi-mat.png'); });