Skip to content

Commit

Permalink
fix: ex.Material no longer does lazy init, requires graphics context
Browse files Browse the repository at this point in the history
  • Loading branch information
eonarheim committed Jan 26, 2024
1 parent ea86b3f commit 08ce7e3
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 54 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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.


<!--------------------------------- DO NOT EDIT BELOW THIS LINE --------------------------------->
Expand Down
2 changes: 1 addition & 1 deletion src/engine/Graphics/Context/ExcaliburGraphicsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export interface ExcaliburGraphicsContext {
* @param options
* @returns
*/
createMaterial(options: MaterialOptions): Material;
createMaterial(options: Omit<MaterialOptions, 'graphicsContext'>): Material;

/**
* Clears the screen with the current background color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ export class ExcaliburGraphicsContext2DCanvas implements ExcaliburGraphicsContex
return this._state.current.material;
}

public createMaterial(_options: MaterialOptions): Material {
public createMaterial(options: Omit<MaterialOptions, 'graphicsContext'>): Material {
// pass
return null;
}
Expand Down
5 changes: 2 additions & 3 deletions src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MaterialOptions, 'graphicsContext'>): Material {
const material = new Material({...options, graphicsContext: this});
return material;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 19 additions & 3 deletions src/engine/Graphics/Context/material.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { Color } from '../../Color';
import { ExcaliburGraphicsContext } from './ExcaliburGraphicsContext';
import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL';
import { Shader } from './shader';
import { Logger } from '../../Util/Log';

export interface MaterialOptions {
/**
* Name the material for debugging
*/
name?: string;

/**
* Excalibur graphics context to create the material (only WebGL is supported at the moment)
*/
graphicsContext?: ExcaliburGraphicsContext;

/**
* Optionally specify a vertex shader
*
Expand Down Expand Up @@ -82,26 +89,35 @@ void main() {
`;

export class Material {
private _logger = Logger.getInstance();
private _name: string;
private _shader: Shader;
private _color: Color = Color.Transparent;
private _initialized = false;
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
});
Expand Down
110 changes: 66 additions & 44 deletions src/spec/MaterialRendererSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,63 @@ 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();
});

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;
Expand All @@ -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');
});
Expand Down Expand Up @@ -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;
Expand All @@ -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');

Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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');

Expand All @@ -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');
});
Expand Down

0 comments on commit 08ce7e3

Please sign in to comment.