Skip to content

Commit

Permalink
fix: Multiple materials at once (#2845)
Browse files Browse the repository at this point in the history
Fixes an issue where rendering multiple materials at once would crash because internal state was not cleared.

Fixes a couple ancillary non-crash errors, error reporting and draw call counting were not accurate.
  • Loading branch information
eonarheim authored Dec 12, 2023
1 parent 1dfb625 commit b247ef5
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const hits = engine.currentScene.physics.rayCast(

### Fixed

- Fixed issue where rendering multiple materials at once would crash the renderer
- Fixed issue where raycasting with more complex collision groups was not working as expected

### Updates
Expand Down
Binary file added sandbox/tests/material/heart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 79 additions & 5 deletions sandbox/tests/material/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/// <reference path="../../lib/excalibur.d.ts" />

// identity tagged template literal lights up glsl-literal vscode plugin
var glsl = x => x;

var game = new ex.Engine({
canvasElementId: 'game',
width: 512,
Expand All @@ -10,11 +13,54 @@ var game = new ex.Engine({
});

var tex = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png', false, ex.ImageFiltering.Pixel);
var heartImage = new ex.ImageSource('./heart.png', false, ex.ImageFiltering.Pixel);
var background = new ex.ImageSource('./stars.png', false, ex.ImageFiltering.Blended);

var loader = new ex.Loader([tex, background]);
var loader = new ex.Loader([tex, heartImage, background]);

var outline = glsl`#version 300 es
precision mediump float;
uniform float iTime;
uniform sampler2D u_graphic;
in vec2 v_uv;
out vec4 fragColor;
vec3 hsv2rgb(vec3 c){
vec4 K=vec4(1.,2./3.,1./3.,3.);
return c.z*mix(K.xxx,clamp(abs(fract(c.x+K.xyz)*6.-K.w)-K.x, 0., 1.),c.y);
}
void main() {
const float TAU = 6.28318530;
const float steps = 4.0; // up/down/left/right pixels
float radius = 2.0;
vec3 outlineColorHSL = vec3(sin(iTime/2.0) * 1., 1., 1.);
vec2 aspect = 1.0 / vec2(textureSize(u_graphic, 0));
for (float i = 0.0; i < TAU; i += TAU / steps) {
// Sample image in a circular pattern
vec2 offset = vec2(sin(i), cos(i)) * aspect * radius;
vec4 col = texture(u_graphic, v_uv + offset);
// Mix outline with background
float alpha = smoothstep(0.5, 0.7, col.a);
fragColor = mix(fragColor, vec4(hsv2rgb(outlineColorHSL), 1.0), alpha); // apply outline
}
// Overlay original texture
vec4 mat = texture(u_graphic, v_uv);
float factor = smoothstep(0.5, 0.7, mat.a);
fragColor = mix(fragColor, mat, factor);
}
`

var fragmentSource = `#version 300 es
var fragmentSource = glsl`#version 300 es
precision mediump float;
// UV coord
Expand Down Expand Up @@ -65,6 +111,11 @@ var swirlMaterial = game.graphicsContext.createMaterial({
fragmentSource
});

var outlineMaterial = game.graphicsContext.createMaterial({
name: 'outline',
fragmentSource: outline
})

var click = ex.vec(0, 0);

game.input.pointers.primary.on('down', evt => {
Expand All @@ -81,16 +132,39 @@ actor.onInitialize = () => {
}
});
actor.graphics.add(sprite);
actor.graphics.material = outlineMaterial;
};

actor.onPostUpdate = (_, delta) => {
time += (delta / 1000);
outlineMaterial.getShader().use();
outlineMaterial.getShader().trySetUniformFloat('iTime', time);
}

var heartActor = new ex.Actor({x: 200, y: 200});
heartActor.onInitialize = () => {
var sprite = heartImage.toSprite();
sprite.scale = ex.vec(4, 4);
heartActor.graphics.add(sprite);
heartActor.graphics.material = outlineMaterial;
}

game.add(heartActor);

game.input.pointers.primary.on('move', evt => {
heartActor.pos = evt.worldPos;
swirlMaterial.getShader().use();
swirlMaterial.getShader().trySetUniformFloatVector('iMouse', evt.worldPos);
});


var backgroundActor = new ex.ScreenElement({x: 0, y: 0, width: 512, height: 512, z: -1});
var time = 0;
backgroundActor.onPostUpdate = (_, delta) => {
time += (delta / 1000);
swirlMaterial.getShader().use();
swirlMaterial.getShader().trySetUniformFloat('iTime', time);
swirlMaterial.getShader().trySetUniformFloatVector('iMouse', click);
// swirlMaterial.getShader().use();
// swirlMaterial.getShader().trySetUniformFloat('iTime', time);
// swirlMaterial.getShader().trySetUniformFloatVector('iMouse', click);
}
backgroundActor.onInitialize = () => {
backgroundActor.graphics.add(background.toSprite());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { vec } from '../../../Math/vector';
import { ImageFiltering } from '../../Filtering';
import { GraphicsDiagnostics } from '../../GraphicsDiagnostics';
import { HTMLImageSource } from '../ExcaliburGraphicsContext';
import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL';
import { QuadIndexBuffer } from '../quad-index-buffer';
Expand Down Expand Up @@ -140,6 +141,9 @@ export class MaterialRenderer implements RendererPlugin {
// apply resolution
shader.trySetUniformFloatVector('u_resolution', vec(this._context.width, this._context.height));

// apply graphic resolution
shader.trySetUniformFloatVector('u_graphic_resolution', vec(imageWidth, imageHeight));

// apply size
shader.trySetUniformFloatVector('u_size', vec(sw, sh));

Expand All @@ -159,6 +163,9 @@ export class MaterialRenderer implements RendererPlugin {

// Draw a single quad
gl.drawElements(gl.TRIANGLES, 6, this._quads.bufferGlType, 0);

GraphicsDiagnostics.DrawnImagesCount++;
GraphicsDiagnostics.DrawCallCount++;
}

private _addImageAsTexture(image: HTMLImageSource) {
Expand Down
3 changes: 3 additions & 0 deletions src/engine/Graphics/Context/shader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,9 @@ export class Shader {
}

private _processSourceForError(source: string, errorInfo: string) {
if (!source) {
return errorInfo;
}
const lines = source.split('\n');
const errorLineStart = errorInfo.search(/\d:\d/);
const errorLineEnd = errorInfo.indexOf(' ', errorLineStart);
Expand Down
1 change: 1 addition & 0 deletions src/engine/Graphics/Context/vertex-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class VertexLayout {
if (!this._shader.compiled) {
throw Error('Shader not compiled, shader must be compiled before defining a vertex layout');
}
this._vertexTotalSizeBytes = 0;
this._layout.length = 0;
const shaderAttributes = this._shader.attributes;
for (const attribute of this._attributes) {
Expand Down
68 changes: 68 additions & 0 deletions src/spec/MaterialRendererSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,72 @@ describe('A Material', () => {
await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas))
.toEqualImage('src/spec/images/MaterialRendererSpec/material-component.png');
});

it('can be draw multiple materials', async () => {
const material1 = new ex.Material({
name: 'material1',
color: ex.Color.Red,
fragmentSource: `#version 300 es
precision mediump float;
uniform vec4 u_color;
out vec4 fragColor;
void main() {
fragColor = u_color;
}`
});

const material2 = new ex.Material({
name: 'material2',
color: ex.Color.Blue,
fragmentSource: `#version 300 es
precision mediump float;
uniform vec4 u_color;
out vec4 fragColor;
void main() {
fragColor = u_color;
}`
});

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');

const loader = new ex.Loader([tex]);

await TestUtils.runToReady(engine, loader);

const actor1 = new ex.Actor({
x: 0,
y: 0,
width: 100,
height: 100
});
actor1.graphics.use(tex.toSprite());
actor1.graphics.material = material1;

const actor2 = new ex.Actor({
x: 100,
y: 100,
width: 100,
height: 100
});
actor2.graphics.use(tex.toSprite());
actor2.graphics.material = material2;

context.clear();
engine.currentScene.add(actor1);
engine.currentScene.add(actor2);
engine.currentScene.draw(context, 100);
context.flush();

expect(context.material).toBe(null);
await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas))
.toEqualImage('src/spec/images/MaterialRendererSpec/multi-mat.png');
});
});
Binary file added src/spec/images/MaterialRendererSpec/multi-mat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b247ef5

Please sign in to comment.