From b247ef51007cf6aafd920ed7543096a2e3839c14 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Tue, 12 Dec 2023 08:53:15 -0600 Subject: [PATCH] fix: Multiple materials at once (#2845) 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. --- CHANGELOG.md | 1 + sandbox/tests/material/heart.png | Bin 0 -> 242 bytes sandbox/tests/material/index.ts | 84 ++++++++++++++++-- .../material-renderer/material-renderer.ts | 7 ++ src/engine/Graphics/Context/shader.ts | 3 + src/engine/Graphics/Context/vertex-layout.ts | 1 + src/spec/MaterialRendererSpec.ts | 68 ++++++++++++++ .../images/MaterialRendererSpec/multi-mat.png | Bin 0 -> 628 bytes 8 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 sandbox/tests/material/heart.png create mode 100644 src/spec/images/MaterialRendererSpec/multi-mat.png 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 0000000000000000000000000000000000000000..2c830cdc201ec590ddd90c87a58b5f30b842362e GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^fNn{1`B?5dxTwA!fnmIY!`1!lU#p@Xu{`08(|NkE-`M!A3J0QhT666;Q6afK- zd(C?zfnuBm9+AZi419+{nDKc2iWH!rzo(01h(>VlxoExv3LGwh(JSB7ALl!~p!Uxf zHMSz5#b@ugZh8DhqAlo+QyB9hUh8ub3sm1|7rvcu(~*DI^Z!ke2}(*H9-OKQ2Zi)+ bEV`o}Y{I6d$~IFEXf%VTtDnm{r-UW|T`p0f literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a431268fd1fd6a38089ff5173a71294b11be43c8 GIT binary patch literal 628 zcmeAS@N?(olHy`uVBq!ia0vp^DImVAAt+aSW-5 zdwXYNFH@iZ%f-6^3XR+Xv;UlE2-qPKrCZ7w)@obtcE$LEh5hl5mU^Fd+Q-}f_w{q! zcRFCg`%e}uf+^t#?oWulowZpZblY8J$FkhjXPZK9mi4pT+`86`J1F(Gtx(GKZMSqh zrsjV482So#v7C7ObJv;pKYQitt#dOzYjT8G*(iHRbV&)fXvy9aRN-;$<#d|L_nt$j zZP787gsexZ=`7S;s>GS7s=Ly!_Cokf@R6Be1HZ!hM2) kh^ZP!sMVf9Yuq<}>FAO~R{PW9z+}YW>FVdQ&MBb@04M+k3jhEB literal 0 HcmV?d00001