diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2fa8abf..2cf31de49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/sandbox/tests/material/heart.png b/sandbox/tests/material/heart.png new file mode 100644 index 000000000..2c830cdc2 Binary files /dev/null and b/sandbox/tests/material/heart.png differ diff --git a/sandbox/tests/material/index.ts b/sandbox/tests/material/index.ts index fb459afc2..e565789f9 100644 --- a/sandbox/tests/material/index.ts +++ b/sandbox/tests/material/index.ts @@ -1,5 +1,8 @@ /// +// identity tagged template literal lights up glsl-literal vscode plugin +var glsl = x => x; + var game = new ex.Engine({ canvasElementId: 'game', width: 512, @@ -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 @@ -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 => { @@ -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()); diff --git a/src/engine/Graphics/Context/material-renderer/material-renderer.ts b/src/engine/Graphics/Context/material-renderer/material-renderer.ts index 75873462f..93fb4de67 100644 --- a/src/engine/Graphics/Context/material-renderer/material-renderer.ts +++ b/src/engine/Graphics/Context/material-renderer/material-renderer.ts @@ -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'; @@ -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)); @@ -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) { diff --git a/src/engine/Graphics/Context/shader.ts b/src/engine/Graphics/Context/shader.ts index ac180ff73..c0715589d 100644 --- a/src/engine/Graphics/Context/shader.ts +++ b/src/engine/Graphics/Context/shader.ts @@ -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); diff --git a/src/engine/Graphics/Context/vertex-layout.ts b/src/engine/Graphics/Context/vertex-layout.ts index bd39193a7..9a070d7d8 100644 --- a/src/engine/Graphics/Context/vertex-layout.ts +++ b/src/engine/Graphics/Context/vertex-layout.ts @@ -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) { diff --git a/src/spec/MaterialRendererSpec.ts b/src/spec/MaterialRendererSpec.ts index 6e62c031b..610b90d99 100644 --- a/src/spec/MaterialRendererSpec.ts +++ b/src/spec/MaterialRendererSpec.ts @@ -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'); + }); }); \ No newline at end of file diff --git a/src/spec/images/MaterialRendererSpec/multi-mat.png b/src/spec/images/MaterialRendererSpec/multi-mat.png new file mode 100644 index 000000000..a431268fd Binary files /dev/null and b/src/spec/images/MaterialRendererSpec/multi-mat.png differ