From 483257767a4cc54d540521461413184cb964f8f1 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Wed, 20 Dec 2023 23:19:37 -0600 Subject: [PATCH] feat: Materials can now use the screen texture (#2849) https://github.com/excaliburjs/Excalibur/assets/612071/ade92ec3-caff-4835-8410-74f4ebbbe421 This PR adds a new feature for materials that allows them to reference the screen texture. They way it works is it's the screen's framebuffer RIGHT before the material draw call (if you include the complete drawing you get the infinite mirror artifact which is pretty unusable) 2 new uniforms * `u_screen_texture` - This is the texture of the screen right before the material draw call * `u_time_ms` - This is the milliseconds since page navigation (`performance.now()` under the hood) 2 new attribute/varyings * `a_screenuv` - The vertex attribute corresponding to the screen uv relative to the current graphic * `v_screenuv` - The fragment varying corresponding to the screen uv relative to the current graphic Finally there is a new convenience api for updating shader values in materials. `.update(shader => {...})` ```typescript game.input.pointers.primary.on('move', evt => { heartActor.pos = evt.worldPos; swirlMaterial.update(shader => { shader.trySetUniformFloatVector('iMouse', evt.worldPos); }); }); ``` --- .vscode/settings.json | 2 +- CHANGELOG.md | 18 +- sandbox/tests/material/index.ts | 224 ++++++++++++++---- .../Context/ExcaliburGraphicsContextWebGL.ts | 19 ++ .../material-renderer/material-renderer.ts | 28 ++- src/engine/Graphics/Context/material.ts | 15 ++ src/spec/MaterialRendererSpec.ts | 92 +++++++ .../MaterialRendererSpec/multiply-comp.png | Bin 0 -> 773 bytes .../MaterialRendererSpec/update-uniform.png | Bin 0 -> 747 bytes 9 files changed, 341 insertions(+), 57 deletions(-) create mode 100644 src/spec/images/MaterialRendererSpec/multiply-comp.png create mode 100644 src/spec/images/MaterialRendererSpec/update-uniform.png diff --git a/.vscode/settings.json b/.vscode/settings.json index 2cfc52275..d721f5bc7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,6 +24,6 @@ }, "typescript.tsdk": "./node_modules/typescript/lib", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index bf66f77f3..6318a22cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,15 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added -- +- Materials have a new convenience method for updating uniforms + ```typescript + game.input.pointers.primary.on('move', evt => { + heartActor.pos = evt.worldPos; + swirlMaterial.update(shader => { + shader.trySetUniformFloatVector('iMouse', evt.worldPos); + }); + }); + ``` ### Fixed @@ -24,7 +32,13 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Updates -- +- Materials can now reference a new uniform for the screen texture and a screen uv attribute in their fragment shaders + * `u_screen_texture` - This is the texture of the screen right before the material draw call + * `a_screenuv` - The vertex attribute corresponding to the screen uv relative to the current graphic + * `v_screenuv` - The fragment varying corresponding to the screen uv relative to the current graphic + +- Materials can now reference the current time in their shaders + * `u_time_ms` - This is the ms since page navigation (performance.now() under the hood) ### Changed diff --git a/sandbox/tests/material/index.ts b/sandbox/tests/material/index.ts index e565789f9..d2af00831 100644 --- a/sandbox/tests/material/index.ts +++ b/sandbox/tests/material/index.ts @@ -1,7 +1,7 @@ /// // identity tagged template literal lights up glsl-literal vscode plugin -var glsl = x => x; +var glsl = x => x[0]; var game = new ex.Engine({ canvasElementId: 'game', @@ -21,42 +21,41 @@ var loader = new ex.Loader([tex, heartImage, background]); var outline = glsl`#version 300 es precision mediump float; -uniform float iTime; - +uniform float u_time_ms; uniform sampler2D u_graphic; in vec2 v_uv; - +in vec2 v_screenuv; 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); + 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.); + const float steps = 4.0; // up/down/left/right pixels + float radius = 2.0; + float time_sec = u_time_ms / 1000.; + vec3 outlineColorHSL = vec3(sin(time_sec/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 + 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 - } - + 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); + vec4 mat = texture(u_graphic, v_uv); + float factor = smoothstep(0.5, 0.7, mat.a); + fragColor = mix(fragColor, mat, factor); } ` @@ -70,7 +69,7 @@ uniform sampler2D u_graphic; uniform vec2 u_resolution; -uniform float iTime; +uniform float u_time_ms; uniform vec2 iMouse; @@ -83,13 +82,12 @@ uniform float u_opacity; out vec4 fragColor; void main() { - vec4 color = u_color; - + vec4 color = u_color; + float time_sec = u_time_ms / 1000.; float effectRadius = .5; - float effectAngle = mod(iTime/2., 2.) * 3.14159; + float effectAngle = mod(time_sec/2., 2.) * 3.14159; vec2 size = u_size.xy; - vec2 center = iMouse.xy / u_size.xy; vec2 uv = v_uv.xy - center; @@ -97,7 +95,6 @@ void main() { float angle = atan(uv.y, uv.x) + effectAngle * smoothstep(effectRadius, 0., len); float radius = length(uv); vec2 newUv = vec2(radius * cos(angle), radius * sin(angle)) + center; - color = texture(u_graphic, newUv); color.rgb = color.rgb * u_opacity; color.a = color.a * u_opacity; @@ -111,10 +108,7 @@ var swirlMaterial = game.graphicsContext.createMaterial({ fragmentSource }); -var outlineMaterial = game.graphicsContext.createMaterial({ - name: 'outline', - fragmentSource: outline -}) + var click = ex.vec(0, 0); @@ -122,7 +116,12 @@ game.input.pointers.primary.on('down', evt => { click = evt.worldPos; // might need to change if you have a camera }); -var actor = new ex.Actor({x: 100, y: 100, width: 50, height: 50}); +var outlineMaterial = game.graphicsContext.createMaterial({ + name: 'outline', + fragmentSource: outline +}) + +var actor = new ex.Actor({ x: 100, y: 100, width: 50, height: 50 }); actor.onInitialize = () => { var sprite = new ex.Sprite({ image: tex, @@ -132,16 +131,10 @@ actor.onInitialize = () => { } }); actor.graphics.add(sprite); - actor.graphics.material = outlineMaterial; }; +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}); +var heartActor = new ex.Actor({ x: 200, y: 200 }); heartActor.onInitialize = () => { var sprite = heartImage.toSprite(); sprite.scale = ex.vec(4, 4); @@ -153,24 +146,151 @@ game.add(heartActor); game.input.pointers.primary.on('move', evt => { heartActor.pos = evt.worldPos; - swirlMaterial.getShader().use(); - swirlMaterial.getShader().trySetUniformFloatVector('iMouse', evt.worldPos); + swirlMaterial.update(shader => { + shader.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); -} +var backgroundActor = new ex.ScreenElement({ x: 0, y: 0, width: 512, height: 512, z: -1 }); + backgroundActor.onInitialize = () => { backgroundActor.graphics.add(background.toSprite()); backgroundActor.graphics.material = swirlMaterial; }; + +// material without graphic!? +var waterFrag = glsl`#version 300 es +precision mediump float; + +#define NUM_NOISE_OCTAVES 20 + +// Precision-adjusted variations of https://www.shadertoy.com/view/4djSRW +float hash(float p) { p = fract(p * 0.011); p *= p + 7.5; p *= p + p; return fract(p); } +float hash(vec2 p) {vec3 p3 = fract(vec3(p.xyx) * 0.13); p3 += dot(p3, p3.yzx + 3.333); return fract((p3.x + p3.y) * p3.z); } + +float noise(float x) { + float i = floor(x); + float f = fract(x); + float u = f * f * (3.0 - 2.0 * f); + return mix(hash(i), hash(i + 1.0), u); +} + + +float noise(vec2 x) { + vec2 i = floor(x); + vec2 f = fract(x); + + // Four corners in 2D of a tile + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + // Simple 2D lerp using smoothstep envelope between the values. + // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), + // mix(c, d, smoothstep(0.0, 1.0, f.x)), + // smoothstep(0.0, 1.0, f.y))); + + // Same code, with the clamps in smoothstep and common subexpressions + // optimized away. + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +float fbm(float x) { + float v = 0.0; + float a = 0.5; + float shift = float(100); + for (int i = 0; i < NUM_NOISE_OCTAVES; ++i) { + v += a * noise(x); + x = x * 2.0 + shift; + a *= 0.5; + } + return v; +} + + +float fbm(vec2 x) { + float v = 0.0; + float a = 0.5; + vec2 shift = vec2(100); + // Rotate to reduce axial bias + mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.50)); + for (int i = 0; i < NUM_NOISE_OCTAVES; ++i) { + v += a * noise(x); + x = rot * x * 2.0 + shift; + a *= 0.5; + } + return v; +} + + + +uniform float u_time_ms; +uniform vec4 u_color; +uniform sampler2D u_graphic; +uniform sampler2D u_screen_texture; + +uniform vec2 u_resolution; // screen resolution +uniform vec2 u_graphic_resolution; // graphic resolution + +in vec2 v_uv; +in vec2 v_screenuv; +out vec4 fragColor; +void main() { + float time_sec = u_time_ms / 1000.; + float wave_amplitude = .525; + float wave_speed = 1.8; + float wave_period = .175; + vec2 scale = vec2(2.5, 8.5); + + float waves = v_uv.y * scale.y + + sin(v_uv.x * scale.x / wave_period - time_sec * wave_speed) * + cos(0.2 * v_uv.x * scale.x /wave_period + time_sec * wave_speed) * + wave_amplitude - wave_amplitude; + + + float distortion = noise(v_uv*scale*vec2(2.1, 1.05) + time_sec * 0.12) * .25 - .125; + + vec2 reflected_screenuv = vec2(v_screenuv.x - distortion, v_screenuv.y); + vec4 screen_color = texture(u_screen_texture, reflected_screenuv); + + vec4 wave_crest_color = vec4(1); + float wave_crest = clamp(smoothstep(0.1, 0.14, waves) - smoothstep(0.018, 0.99, waves), 0., 1.); + + fragColor.a = smoothstep(0.1, 0.12, waves); + vec3 mixColor = (u_color.rgb * u_color.a); // pre-multiplied alpha + + fragColor.rgb = mix(screen_color.rgb, mixColor, u_color.a)*fragColor.a + (wave_crest_color.rgb * wave_crest); +}`; + + +var waterMaterial = game.graphicsContext.createMaterial({ + name: 'water', + fragmentSource: waterFrag, + color: ex.Color.fromRGB(55, 0, 200, .6) +}); +var reflection = new ex.Actor({ + x: 0, + y: game.screen.resolution.height/2, + anchor: ex.vec(0, 0), + width: 512, + height: game.screen.resolution.height/2, + coordPlane: ex.CoordPlane.Screen, + color: ex.Color.Red +}); + +reflection.graphics.material = waterMaterial; +reflection.z = 99; + game.add(actor); game.add(backgroundActor); -game.start(loader); \ No newline at end of file +game.add(reflection); + + +game.start(loader).then(async () => { + // const image = await game.screenshot(true); + // document.body.appendChild(image); +}); \ No newline at end of file diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index fdae4d654..84cd18151 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -130,6 +130,8 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { public textureLoader: TextureLoader; + public materialScreenTexture: WebGLTexture; + public get z(): number { return this._state.current.z; } @@ -229,6 +231,16 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this.register(new PointRenderer()); this.register(new LineRenderer()); + + this.materialScreenTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.materialScreenTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + gl.bindTexture(gl.TEXTURE_2D, null); + this._screenRenderer = new ScreenPassPainter(gl); this._renderTarget = new RenderTarget({ @@ -562,6 +574,13 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { currentRenderer = this._renderers.get(currentRendererName); } + // ! hack to grab screen texture before materials run because they might want it + if (currentRenderer instanceof MaterialRenderer && this.material.isUsingScreenTexture) { + const gl = this.__gl; + gl.bindTexture(gl.TEXTURE_2D, this.materialScreenTexture); + gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, this.width, this.height, 0); + this._renderTarget.use(); + } // If we are still using the same renderer we can add to the current batch currentRenderer.draw(...this._drawCalls[i].args); } diff --git a/src/engine/Graphics/Context/material-renderer/material-renderer.ts b/src/engine/Graphics/Context/material-renderer/material-renderer.ts index 93fb4de67..4c5c71244 100644 --- a/src/engine/Graphics/Context/material-renderer/material-renderer.ts +++ b/src/engine/Graphics/Context/material-renderer/material-renderer.ts @@ -26,7 +26,7 @@ export class MaterialRenderer implements RendererPlugin { // Setup memory layout this._buffer = new VertexBuffer({ gl, - size: 4 * 4, // 4 components * 4 verts + size: 6 * 4, // 6 components * 4 verts type: 'dynamic' }); @@ -36,7 +36,8 @@ export class MaterialRenderer implements RendererPlugin { vertexBuffer: this._buffer, attributes: [ ['a_position', 2], - ['a_uv', 2] + ['a_uv', 2], + ['a_screenuv', 2] ] }); @@ -101,29 +102,44 @@ export class MaterialRenderer implements RendererPlugin { const uvx1 = (sx + sw - 0.01) / imageWidth; const uvy1 = (sy + sh - 0.01) / imageHeight; + const topLeftScreen = transform.getPosition(); + const bottomRightScreen = topLeftScreen.add(bottomRight); + const screenUVX0 = topLeftScreen.x / this._context.width; + const screenUVY0 = topLeftScreen.y / this._context.height; + const screenUVX1 = bottomRightScreen.x / this._context.width; + const screenUVY1 = bottomRightScreen.y / this._context.height; + // (0, 0) - 0 vertexBuffer[vertexIndex++] = topLeft.x; vertexBuffer[vertexIndex++] = topLeft.y; vertexBuffer[vertexIndex++] = uvx0; vertexBuffer[vertexIndex++] = uvy0; + vertexBuffer[vertexIndex++] = screenUVX0; + vertexBuffer[vertexIndex++] = screenUVY0; // (0, 1) - 1 vertexBuffer[vertexIndex++] = bottomLeft.x; vertexBuffer[vertexIndex++] = bottomLeft.y; vertexBuffer[vertexIndex++] = uvx0; vertexBuffer[vertexIndex++] = uvy1; + vertexBuffer[vertexIndex++] = screenUVX0; + vertexBuffer[vertexIndex++] = screenUVY1; // (1, 0) - 2 vertexBuffer[vertexIndex++] = topRight.x; vertexBuffer[vertexIndex++] = topRight.y; vertexBuffer[vertexIndex++] = uvx1; vertexBuffer[vertexIndex++] = uvy0; + vertexBuffer[vertexIndex++] = screenUVX1; + vertexBuffer[vertexIndex++] = screenUVY0; // (1, 1) - 3 vertexBuffer[vertexIndex++] = bottomRight.x; vertexBuffer[vertexIndex++] = bottomRight.y; vertexBuffer[vertexIndex++] = uvx1; vertexBuffer[vertexIndex++] = uvy1; + vertexBuffer[vertexIndex++] = screenUVX1; + vertexBuffer[vertexIndex++] = screenUVY1; // This creates and uploads the texture if not already done const texture = this._addImageAsTexture(image); @@ -135,6 +151,9 @@ export class MaterialRenderer implements RendererPlugin { // apply layout and geometry this._layout.use(true); + // apply time in ms since the page (performance.now()) + shader.trySetUniformFloat('u_time_ms', performance.now()); + // apply opacity shader.trySetUniformFloat('u_opacity', opacity); @@ -158,6 +177,11 @@ export class MaterialRenderer implements RendererPlugin { gl.bindTexture(gl.TEXTURE_2D, texture); shader.trySetUniformInt('u_graphic', 0); + // bind the screen texture + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, this._context.materialScreenTexture); + shader.trySetUniformInt('u_screen_texture', 1); + // bind quad index buffer this._quads.bind(); diff --git a/src/engine/Graphics/Context/material.ts b/src/engine/Graphics/Context/material.ts index a4153920a..1c710c26f 100644 --- a/src/engine/Graphics/Context/material.ts +++ b/src/engine/Graphics/Context/material.ts @@ -65,6 +65,9 @@ in vec2 a_position; in vec2 a_uv; out vec2 v_uv; +in vec2 a_screenuv; +out vec2 v_screenuv; + uniform mat4 u_matrix; uniform mat4 u_transform; @@ -74,6 +77,7 @@ void main() { // Pass through the UV coord to the fragment shader v_uv = a_uv; + v_screenuv = a_screenuv; } `; @@ -109,6 +113,17 @@ export class Material { return this._name ?? 'anonymous material'; } + get isUsingScreenTexture() { + return this._fragmentSource.includes('u_screen_texture'); + } + + update(callback: (shader: Shader) => any) { + if (this._shader) { + this._shader.use(); + callback(this._shader); + } + } + getShader(): Shader | null { return this._shader; } diff --git a/src/spec/MaterialRendererSpec.ts b/src/spec/MaterialRendererSpec.ts index 610b90d99..8efc60a5c 100644 --- a/src/spec/MaterialRendererSpec.ts +++ b/src/spec/MaterialRendererSpec.ts @@ -76,6 +76,98 @@ describe('A Material', () => { .toEqualImage('src/spec/images/MaterialRendererSpec/material.png'); }); + it('can draw the screen texture', async () => { + + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.ExcaliburBlue, + smoothing: false, + snapToPixel: true + }); + + const material = context.createMaterial({ + name: 'test', + color: ex.Color.Red, + fragmentSource: `#version 300 es + precision mediump float; + // UV coord + in vec2 v_uv; + in vec2 v_screenuv; + uniform sampler2D u_screen_texture; + uniform sampler2D u_graphic; + + out vec4 fragColor; + void main() { + fragColor = texture(u_screen_texture, v_screenuv) * texture(u_graphic, v_uv); + }` + }); + + 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(); + + expect(context.material).toBe(null); + await expectAsync(TestUtils.flushWebGLCanvasTo2D(canvas)) + .toEqualImage('src/spec/images/MaterialRendererSpec/multiply-comp.png'); + }); + + it('can update uniforms with the .update()', async () => { + + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.ExcaliburBlue, + smoothing: false, + snapToPixel: true + }); + + const material = context.createMaterial({ + name: 'test', + color: ex.Color.Red, + fragmentSource: `#version 300 es + precision mediump float; + // UV coord + in vec2 v_uv; + in vec2 v_screenuv; + uniform sampler2D u_screen_texture; + uniform sampler2D u_graphic; + uniform vec4 customcolor; + + out vec4 fragColor; + void main() { + fragColor = texture(u_screen_texture, v_screenuv) * texture(u_graphic, v_uv) - customcolor; + }` + }); + + const tex = new ex.ImageSource('src/spec/images/MaterialRendererSpec/sword.png'); + await tex.load(); + + context.clear(); + context.save(); + context.material = material; + material.update(shader => { + shader.setUniformFloatColor('customcolor', ex.Color.Red); + }); + context.drawImage(tex.image, 0, 0); + context.flush(); + context.restore(); + + expect(context.material).toBe(null); + await expectAsync(TestUtils.flushWebGLCanvasTo2D(canvas)) + .toEqualImage('src/spec/images/MaterialRendererSpec/update-uniform.png'); + }); + it('can be created with a custom fragment shader with the graphics component', async () => { const material = new ex.Material({ name: 'test', diff --git a/src/spec/images/MaterialRendererSpec/multiply-comp.png b/src/spec/images/MaterialRendererSpec/multiply-comp.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f16670a4e8a5184ec236710baccce932f46460 GIT binary patch literal 773 zcmeAS@N?(olHy`uVBq!ia0vp^DImVA}8L;uum9 z_jb-+uOkj3t+ChZ69sj|SPovxnt#wqP-pEL9U!RXi+9){KHp)(chMY?BYut>1I1z^ z4(2U8@n*)I*^__VJbGv59LxK2?)Zki`QpFsSn!_{78e*hc)H^i87@9z6fl%yVCTQX9=mn_%IQRdztisXnCV7#n?|+8! zm$M7$E0`O8EcW{IZgp1kp6ARR+aGtnW@J%V$b4s0R^84Ym6Z`Y)Q_(=KKQe>_w1ox z4A?F1?ACKj8xT7ondx|SCcBeJYYBV{;%ee8GtxH00?<58R!x*5E hWTyeB)((jY@=4a({xfF&I}1$L44$rjF6*2UngChKMU|QneNcUTfJebdXJ0FWOEpHcOG6GKw|tqWWYEw^(0*#vGYU+Xho=AEtsScJv+it?djHe8 zyq|s2`UPf&Ka0Koz01yMZ#vHGvEkvh5{8X3TzTwZ=0}HJXX||LM%~(1ecUlx~^64jo5}SYmY7~+m`}-JN YX3TWZSN(k(n0gsJUHx3vIVCg!0J%dqHvj+t literal 0 HcmV?d00001