diff --git a/package-lock.json b/package-lock.json
index 7d285d57..35cf6a70 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"babel-loader": "9.2.1",
+ "concurrently": "9.1.2",
"copy-webpack-plugin": "12.0.2",
"copyfiles": "2.4.1",
"coveralls": "3.1.1",
@@ -9038,6 +9039,133 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/concurrently": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
+ "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "lodash": "^4.17.21",
+ "rxjs": "^7.8.1",
+ "shell-quote": "^1.8.1",
+ "supports-color": "^8.1.1",
+ "tree-kill": "^1.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/concurrently/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/concurrently/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/concurrently/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/concurrently/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/concurrently/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/concurrently/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/concurrently/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/concurrently/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
@@ -18114,6 +18242,15 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -18537,6 +18674,18 @@
"node": ">=8"
}
},
+ "node_modules/shell-quote": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
+ "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/shiki": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz",
@@ -19883,6 +20032,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
"node_modules/trim-right": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
diff --git a/package.json b/package.json
index 167c0586..5f998f89 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"appveyor": "npm run all:ci && npm run nuget 0.0.1",
"browserstack": "karma start karma.conf.browsers.js",
"nuget": ".\\src\\tools\\NuGet.exe pack Excalibur.nuspec -OutputDirectory .\\build\\nuget -version",
+ "dev": "concurrently --names \"umd,esm\" \"npm:start\" \"npm:start:esm\"",
"start": "npm run core:watch",
"start:esm": "npm run core:bundle:esm -- --watch",
"build": "npm run core",
@@ -97,6 +98,7 @@
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"babel-loader": "9.2.1",
+ "concurrently": "9.1.2",
"copy-webpack-plugin": "12.0.2",
"copyfiles": "2.4.1",
"coveralls": "3.1.1",
diff --git a/sandbox/tests/occluder/index.ts b/sandbox/tests/occluder/index.ts
index d436e9a9..8fa3b704 100644
--- a/sandbox/tests/occluder/index.ts
+++ b/sandbox/tests/occluder/index.ts
@@ -44,8 +44,8 @@ class LightingPostProcessor implements ex.PostProcessor {
console.log(this.texture);
}
- initialize(gl: WebGL2RenderingContext): void {
- this._shader = new ex.ScreenShader(gl, shader);
+ initialize(graphicsContext: ex.ExcaliburGraphicsContextWebGL): void {
+ this._shader = new ex.ScreenShader(graphicsContext, shader);
}
getLayout(): ex.VertexLayout {
return this._shader.getLayout();
diff --git a/sandbox/tests/postprocessor/main.ts b/sandbox/tests/postprocessor/main.ts
index b12d3e53..dbfaebe1 100644
--- a/sandbox/tests/postprocessor/main.ts
+++ b/sandbox/tests/postprocessor/main.ts
@@ -72,8 +72,8 @@ void main()
class CRTPostProcessors implements ex.PostProcessor {
private _shader: ex.ScreenShader;
constructor() {}
- initialize(gl: WebGL2RenderingContext): void {
- this._shader = new ex.ScreenShader(gl, crtFragmentSource);
+ initialize(graphicsContext: ex.ExcaliburGraphicsContextWebGL): void {
+ this._shader = new ex.ScreenShader(graphicsContext, crtFragmentSource);
}
getShader(): ex.Shader {
return this._shader.getShader();
@@ -90,9 +90,9 @@ class CRTPostProcessors implements ex.PostProcessor {
class GrayScalePostProcessor implements ex.PostProcessor {
private _shader: ex.ScreenShader;
- initialize(gl: WebGL2RenderingContext): void {
+ initialize(graphicsContext: ex.ExcaliburGraphicsContextWebGL): void {
this._shader = new ex.ScreenShader(
- gl,
+ graphicsContext,
`#version 300 es
precision mediump float;
// our texture
diff --git a/sandbox/tests/uniform-buffer/index.html b/sandbox/tests/uniform-buffer/index.html
new file mode 100644
index 00000000..3f46a103
--- /dev/null
+++ b/sandbox/tests/uniform-buffer/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Flash Shader
+
+
+
+
+
+
diff --git a/sandbox/tests/uniform-buffer/index.ts b/sandbox/tests/uniform-buffer/index.ts
new file mode 100644
index 00000000..27758f68
--- /dev/null
+++ b/sandbox/tests/uniform-buffer/index.ts
@@ -0,0 +1,84 @@
+var game = new ex.Engine({
+ width: 800,
+ height: 800
+});
+
+var tex = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png', false, ex.ImageFiltering.Pixel);
+
+var actor = new ex.Actor({
+ pos: ex.vec(0, 0),
+ anchor: ex.vec(0, 0),
+ width: 800,
+ height: 800,
+ color: ex.Color.Black
+});
+
+game.add(actor);
+
+var material = game.graphicsContext.createMaterial({
+ name: 'flash-material',
+ fragmentSource: `#version 300 es
+ precision mediump float;
+
+ struct Light {
+ vec2 pos;
+ float radius;
+ vec4 color;
+ };
+
+ layout(std140) uniform Lighting {
+ Light lights[2];
+ };
+
+ in vec2 v_uv;
+ out vec4 color;
+
+ void main() {
+ float distanceToLights = 1.0;
+ vec4 finalColor = vec4(0.0, 0.0, 0.0, 1.0);
+ for (int i = 0; i < 2; i++) {
+ float dist = length(lights[i].pos - v_uv);
+ dist = smoothstep(lights[i].radius-.2, lights[i].radius+.2, dist);
+ finalColor += lights[i].color * (1.0 - dist);
+ }
+
+ color = finalColor;
+ // premultiply alpha
+ color.rgb = color.rgb * color.a;
+ }`,
+ uniforms: {
+ // prettier-ignore
+ Lighting: new Float32Array([
+ 0.5, 0.5, 0.1, 0.1, // light 1 pos
+ 0, 1, 0, 1, // light 1 color
+ 1, 1, 0.1, 0.1, // light2 pos
+ 0, 0, 1, 1 // light 2 color
+ ])
+ }
+}) as ex.Material;
+
+actor.graphics.material = material;
+ex.coroutine(
+ game,
+ function* () {
+ let time = 0;
+ while (true) {
+ const elapsed = yield;
+ time += elapsed / 1000;
+ const x1 = Math.cos(time);
+ const y1 = Math.sin(time);
+ const x2 = Math.cos(-time);
+ const y2 = Math.sin(-time);
+
+ // prettier-ignore
+ material.uniforms.Lighting = new Float32Array([
+ 0.2 * x1 + 0.2, 0.2 * y1 + 0.2, 0.1, 0,
+ 0, 1, 0, 1,
+ 0.5 * x2 + 0.5, 0.5 * y2 + 0.5, 0.1, 0,
+ 0, 0, 1, 1
+ ]);
+ }
+ }.bind(this)
+);
+
+game.start(new ex.Loader([tex]));
diff --git a/site/docs/04-graphics/99.0-custom-renderer-plugins.mdx b/site/docs/04-graphics/99.0-custom-renderer-plugins.mdx
index cbd885b9..22c6be8f 100644
--- a/site/docs/04-graphics/99.0-custom-renderer-plugins.mdx
+++ b/site/docs/04-graphics/99.0-custom-renderer-plugins.mdx
@@ -101,6 +101,7 @@ export class PointRenderer implements RendererPlugin {
this._gl = gl;
this._context = context;
this._shader = new Shader({
+ graphicsContext: context,
vertexSource: pointVertexSource,
fragmentSource: pointFragmentSource
});
diff --git a/src/engine/GarbageCollector.ts b/src/engine/GarbageCollector.ts
index d3e8ab45..587a27f9 100644
--- a/src/engine/GarbageCollector.ts
+++ b/src/engine/GarbageCollector.ts
@@ -108,6 +108,10 @@ export class GarbageCollector {
}
}
+ running(): boolean {
+ return this._running;
+ }
+
/**
* Starts the garbage collection loop
*/
diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
index dc4e4af0..a2431d31 100644
--- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
+++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
@@ -122,6 +122,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
private _screenRenderer!: ScreenPassPainter;
private _postprocessors: PostProcessor[] = [];
+
/**
* Meant for internal use only. Access the internal context at your own risk and no guarantees this will exist in the future.
* @internal
@@ -339,7 +340,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.bindTexture(gl.TEXTURE_2D, null);
- this._screenRenderer = new ScreenPassPainter(gl);
+ this._screenRenderer = new ScreenPassPainter(this);
this._renderTarget = new RenderTarget({
gl,
@@ -610,7 +611,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
public addPostProcessor(postprocessor: PostProcessor) {
this._postprocessors.push(postprocessor);
- postprocessor.initialize(this.__gl);
+ postprocessor.initialize(this);
}
public removePostProcessor(postprocessor: PostProcessor) {
@@ -629,7 +630,7 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
for (const postprocessor of this._postprocessors) {
const shader = postprocessor.getShader();
shader.use();
- const uniforms = shader.getUniforms();
+ const uniforms = shader.getUniformDefinitions();
this._totalPostProcessorTime += elapsed;
if (uniforms.find((u) => u.name === 'u_time_ms')) {
@@ -666,13 +667,16 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
return material;
}
- public createShader(options: Omit): Shader {
- const gl = this.__gl;
- const { vertexSource, fragmentSource } = options;
+ public createShader(options: Omit): Shader {
+ const { name, vertexSource, fragmentSource, uniforms, images, startingTextureSlot } = options;
const shader = new Shader({
- gl,
+ name,
+ graphicsContext: this,
vertexSource,
- fragmentSource
+ fragmentSource,
+ uniforms,
+ images,
+ startingTextureSlot
});
shader.compile();
return shader;
diff --git a/src/engine/Graphics/Context/circle-renderer/circle-renderer.ts b/src/engine/Graphics/Context/circle-renderer/circle-renderer.ts
index 09e802a6..6543542c 100644
--- a/src/engine/Graphics/Context/circle-renderer/circle-renderer.ts
+++ b/src/engine/Graphics/Context/circle-renderer/circle-renderer.ts
@@ -31,7 +31,7 @@ export class CircleRenderer implements RendererPlugin {
this._gl = gl;
this._context = context;
this._shader = new Shader({
- gl,
+ graphicsContext: context,
fragmentSource: frag,
vertexSource: vert
});
diff --git a/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.ts b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.ts
index dcbad2f2..b1489e6b 100644
--- a/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.ts
+++ b/src/engine/Graphics/Context/image-renderer-v2/image-renderer-v2.ts
@@ -64,7 +64,7 @@ export class ImageRendererV2 implements RendererPlugin {
const transformedFrag = this._transformFragmentSource(frag, this._maxTextures);
// Compile shader
this._shader = new Shader({
- gl,
+ graphicsContext: context,
fragmentSource: transformedFrag,
vertexSource: vert
});
diff --git a/src/engine/Graphics/Context/image-renderer/image-renderer.ts b/src/engine/Graphics/Context/image-renderer/image-renderer.ts
index 1a37fcab..f6b48263 100644
--- a/src/engine/Graphics/Context/image-renderer/image-renderer.ts
+++ b/src/engine/Graphics/Context/image-renderer/image-renderer.ts
@@ -53,15 +53,13 @@ export class ImageRenderer implements RendererPlugin {
initialize(gl: WebGL2RenderingContext, context: ExcaliburGraphicsContextWebGL): void {
this._gl = gl;
this._context = context;
- // Transform shader source
- // FIXME: PIXEL 6 complains `ERROR: Expression too complex.` if we use it's reported max texture units, 125 seems to work for now...
const maxTexture = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
const maxComplexity = getMaxShaderComplexity(gl, maxTexture);
this._maxTextures = Math.min(maxTexture, maxComplexity);
const transformedFrag = this._transformFragmentSource(frag, this._maxTextures);
// Compile shader
this._shader = new Shader({
- gl,
+ graphicsContext: context,
fragmentSource: transformedFrag,
vertexSource: vert
});
diff --git a/src/engine/Graphics/Context/line-renderer/line-renderer.ts b/src/engine/Graphics/Context/line-renderer/line-renderer.ts
index e1213dfe..309003ff 100644
--- a/src/engine/Graphics/Context/line-renderer/line-renderer.ts
+++ b/src/engine/Graphics/Context/line-renderer/line-renderer.ts
@@ -27,7 +27,7 @@ export class LineRenderer implements RendererPlugin {
this._gl = gl;
this._context = context;
this._shader = new Shader({
- gl,
+ graphicsContext: context,
vertexSource: lineVertexSource,
fragmentSource: lineFragmentSource
});
diff --git a/src/engine/Graphics/Context/material-renderer/material-renderer.ts b/src/engine/Graphics/Context/material-renderer/material-renderer.ts
index 494d1a93..aa4c1534 100644
--- a/src/engine/Graphics/Context/material-renderer/material-renderer.ts
+++ b/src/engine/Graphics/Context/material-renderer/material-renderer.ts
@@ -192,12 +192,11 @@ export class MaterialRenderer implements RendererPlugin {
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 any additional textures in the material
- material.uploadAndBind(gl);
+ if (material.isUsingScreenTexture) {
+ 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 45c679d3..d3365702 100644
--- a/src/engine/Graphics/Context/material.ts
+++ b/src/engine/Graphics/Context/material.ts
@@ -1,11 +1,10 @@
import { Color } from '../../Color';
import { ExcaliburGraphicsContext } from './ExcaliburGraphicsContext';
import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL';
-import { Shader } from './shader';
+import { Shader, UniformDictionary } from './shader';
import { Logger } from '../../Util/Log';
-import { ImageSource, ImageSourceAttributeConstants } from '../ImageSource';
-import { ImageFiltering, parseImageFiltering } from '../Filtering';
-import { parseImageWrapping } from '../Wrapping';
+import { ImageSource } from '../ImageSource';
+import { ImageFiltering } from '../Filtering';
export interface MaterialOptions {
/**
@@ -75,6 +74,11 @@ export interface MaterialOptions {
* Specify a dictionary of uniform sampler names to ImageSource
*/
images?: Record;
+
+ /**
+ * Optionally set starting uniforms on a shader
+ */
+ uniforms?: UniformDictionary;
}
const defaultVertexSource = `#version 300 es
@@ -112,49 +116,57 @@ export class Material {
private _fragmentSource: string;
private _vertexSource: string;
- private _images = new Map();
- private _textures = new Map();
- private _maxTextureSlots!: number;
- private _graphicsContext!: ExcaliburGraphicsContextWebGL;
+ private _images: Record = {};
+ private _uniforms: UniformDictionary = {};
constructor(options: MaterialOptions) {
- const { color, name, vertexSource, fragmentSource, graphicsContext, images } = options;
+ const { color, name, vertexSource, fragmentSource, graphicsContext, images, uniforms } = options;
+
this._name = name ?? 'anonymous material';
this._vertexSource = vertexSource ?? defaultVertexSource;
this._fragmentSource = fragmentSource;
this._color = color ?? this._color;
+ this._uniforms = uniforms ?? this._uniforms;
+ this._images = images ?? this._images;
+
if (!graphicsContext) {
throw Error(`Material ${name} must be provided an excalibur webgl graphics context`);
}
+
if (graphicsContext instanceof ExcaliburGraphicsContextWebGL) {
- this._graphicsContext = graphicsContext;
this._initialize(graphicsContext);
} else {
this._logger.warn(`Material ${name} was created in 2D Canvas mode, currently only WebGL is supported`);
}
-
- if (images) {
- for (const key in images) {
- this.addImageSource(key, images[key]);
- }
- }
}
private _initialize(graphicsContextWebGL: ExcaliburGraphicsContextWebGL) {
if (this._initialized) {
return;
}
- const gl = graphicsContextWebGL.__gl;
- // max texture slots - 2 for the graphic texture and screen texture
- this._maxTextureSlots = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) - 2;
+
this._shader = graphicsContextWebGL.createShader({
+ name: this._name,
vertexSource: this._vertexSource,
- fragmentSource: this._fragmentSource
+ fragmentSource: this._fragmentSource,
+ uniforms: this._uniforms,
+ images: this._images,
+ // max texture slots
+ // - 2 for the graphic texture and screen texture
+ // - 1 if just graphic
+ startingTextureSlot: this.isUsingScreenTexture ? 2 : 1
});
- this._shader.compile();
this._initialized = true;
}
+ public get uniforms(): UniformDictionary {
+ return this._shader.uniforms;
+ }
+
+ public get images(): Record {
+ return this._shader.images;
+ }
+
get color(): Color {
return this._color;
}
@@ -164,7 +176,7 @@ export class Material {
}
get name() {
- return this._name ?? 'anonymous material';
+ return this._name;
}
get isUsingScreenTexture() {
@@ -182,66 +194,12 @@ export class Material {
return this._shader;
}
- addImageSource(textureUniformName: string, image: ImageSource) {
- if (this._images.size < this._maxTextureSlots) {
- this._images.set(textureUniformName, image);
- } else {
- this._logger.warn(
- `Max number texture slots ${this._maxTextureSlots} have been reached for material "${this.name}", ` +
- `no more textures will be uploaded due to hardware constraints.`
- );
- }
- }
-
- removeImageSource(textureName: string) {
- const image = this._images.get(textureName);
- this._graphicsContext.textureLoader.delete(image!.image);
- this._images.delete(textureName);
+ addImageSource(samplerName: string, image: ImageSource) {
+ this._shader.addImageSource(samplerName, image);
}
- private _loadImageSource(image: ImageSource) {
- const imageElement = image.image;
- const maybeFiltering = imageElement.getAttribute(ImageSourceAttributeConstants.Filtering);
- const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : undefined;
- const wrapX = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingX) as any);
- const wrapY = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingY) as any);
-
- const force = imageElement.getAttribute('forceUpload') === 'true' ? true : false;
- const texture = this._graphicsContext.textureLoader.load(
- imageElement,
- {
- filtering,
- wrapping: { x: wrapX, y: wrapY }
- },
- force
- );
- // remove force attribute after upload
- imageElement.removeAttribute('forceUpload');
- if (!this._textures.has(image)) {
- this._textures.set(image, texture!);
- }
-
- return texture;
- }
-
- uploadAndBind(gl: WebGL2RenderingContext, startingTextureSlot: number = 2) {
- let textureSlot = startingTextureSlot;
- for (const [textureName, image] of this._images.entries()) {
- if (!image.isLoaded()) {
- this._logger.warnOnce(
- `Image named ${textureName} in material ${this.name} not loaded, nothing will be uploaded to the shader.` +
- ` Did you forget to add this to a loader? https://excaliburjs.com/docs/loaders/`
- );
- continue;
- } // skip unloaded images, maybe warn
- const texture = this._loadImageSource(image);
-
- gl.activeTexture(gl.TEXTURE0 + textureSlot);
- gl.bindTexture(gl.TEXTURE_2D, texture);
- this._shader.trySetUniformInt(textureName, textureSlot);
-
- textureSlot++;
- }
+ removeImageSource(samplerName: string) {
+ this._shader.removeImageSource(samplerName);
}
use() {
diff --git a/src/engine/Graphics/Context/particle-renderer/particle-renderer.ts b/src/engine/Graphics/Context/particle-renderer/particle-renderer.ts
index 101cf6de..c43820e9 100644
--- a/src/engine/Graphics/Context/particle-renderer/particle-renderer.ts
+++ b/src/engine/Graphics/Context/particle-renderer/particle-renderer.ts
@@ -23,7 +23,7 @@ export class ParticleRenderer implements RendererPlugin {
this._gl = gl;
this._context = context;
this._shader = new Shader({
- gl,
+ graphicsContext: context,
vertexSource: particleVertexSource,
fragmentSource: particleFragmentSource,
onPreLink: (program) => {
diff --git a/src/engine/Graphics/Context/point-renderer/point-renderer.ts b/src/engine/Graphics/Context/point-renderer/point-renderer.ts
index 4058c3b1..d9a81961 100644
--- a/src/engine/Graphics/Context/point-renderer/point-renderer.ts
+++ b/src/engine/Graphics/Context/point-renderer/point-renderer.ts
@@ -24,7 +24,7 @@ export class PointRenderer implements RendererPlugin {
this._gl = gl;
this._context = context;
this._shader = new Shader({
- gl,
+ graphicsContext: context,
vertexSource: pointVertexSource,
fragmentSource: pointFragmentSource
});
diff --git a/src/engine/Graphics/Context/rectangle-renderer/rectangle-renderer.ts b/src/engine/Graphics/Context/rectangle-renderer/rectangle-renderer.ts
index 8095b8a9..c13b1462 100644
--- a/src/engine/Graphics/Context/rectangle-renderer/rectangle-renderer.ts
+++ b/src/engine/Graphics/Context/rectangle-renderer/rectangle-renderer.ts
@@ -31,7 +31,7 @@ export class RectangleRenderer implements RendererPlugin {
this._context = context;
// https://stackoverflow.com/questions/59197671/glsl-rounded-rectangle-with-variable-border
this._shader = new Shader({
- gl,
+ graphicsContext: context,
fragmentSource: frag,
vertexSource: vert
});
diff --git a/src/engine/Graphics/Context/screen-pass-painter/screen-pass-painter.ts b/src/engine/Graphics/Context/screen-pass-painter/screen-pass-painter.ts
index db25bf63..248186fb 100644
--- a/src/engine/Graphics/Context/screen-pass-painter/screen-pass-painter.ts
+++ b/src/engine/Graphics/Context/screen-pass-painter/screen-pass-painter.ts
@@ -4,6 +4,7 @@ import { Shader } from '../shader';
import { VertexBuffer } from '../vertex-buffer';
import { VertexLayout } from '../vertex-layout';
import { PostProcessor } from '../../PostProcessor/PostProcessor';
+import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL';
/**
* This is responsible for painting the entire screen during the render passes
@@ -13,10 +14,11 @@ export class ScreenPassPainter {
private _shader: Shader;
private _buffer: VertexBuffer;
private _layout: VertexLayout;
- constructor(gl: WebGL2RenderingContext) {
+ constructor(context: ExcaliburGraphicsContextWebGL) {
+ const gl = context.__gl;
this._gl = gl;
this._shader = new Shader({
- gl,
+ graphicsContext: context,
vertexSource: screenVertex,
fragmentSource: screenFragment
});
diff --git a/src/engine/Graphics/Context/shader.ts b/src/engine/Graphics/Context/shader.ts
index cbc2713c..13388a0c 100644
--- a/src/engine/Graphics/Context/shader.ts
+++ b/src/engine/Graphics/Context/shader.ts
@@ -1,30 +1,178 @@
-import { AffineMatrix, Color, Logger, Vector } from '../..';
+import {
+ AffineMatrix,
+ Color,
+ ExcaliburGraphicsContext,
+ ExcaliburGraphicsContextWebGL,
+ ImageSource,
+ ImageSourceAttributeConstants,
+ Logger,
+ parseImageFiltering,
+ parseImageWrapping,
+ TextureLoader,
+ Vector
+} from '../..';
import { Matrix } from '../../Math/matrix';
+import { watch } from '../../Util/Watch';
+import { UniformBuffer } from './uniform-buffer';
import { getAttributeComponentSize, getAttributePointerType } from './webgl-util';
-/**
+export type UniformDictionary = Record<
+ string,
+ number | boolean | Vector | Color | AffineMatrix | Float32Array | [uniformData: Float32Array, bindingPoint: number]
+>;
+/*
* List of the possible glsl uniform types
*/
export type UniformTypeNames =
| 'uniform1f'
| 'uniform1i'
+ | 'uniform1ui'
| 'uniform2f'
| 'uniform2i'
+ | 'uniform2ui'
| 'uniform3f'
| 'uniform3i'
+ | 'uniform3ui'
| 'uniform4f'
| 'uniform4i'
+ | 'uniform4ui'
| 'uniform1fv'
| 'uniform1iv'
+ | 'uniform1uiv'
| 'uniform2fv'
| 'uniform2iv'
+ | 'uniform2uiv'
| 'uniform3fv'
| 'uniform3iv'
+ | 'uniform3uiv'
| 'uniform4fv'
| 'uniform4iv'
+ | 'uniform4uiv'
| 'uniformMatrix2fv'
+ | 'uniformMatrix2x3fv'
+ | 'uniformMatrix2x4fv'
| 'uniformMatrix3fv'
- | 'uniformMatrix4fv';
+ | 'uniformMatrix3x2fv'
+ | 'uniformMatrix3x4fv'
+ | 'uniformMatrix4fv'
+ | 'uniformMatrix4x2fv'
+ | 'uniformMatrix4x3fv';
+
+/**
+ *
+ */
+export function glTypeToUniformTypeName(gl: WebGL2RenderingContext, glType: number): UniformTypeNames {
+ switch (glType) {
+ case gl.FLOAT: {
+ return 'uniform1f';
+ }
+ case gl.FLOAT_VEC2: {
+ return 'uniform2f';
+ }
+ case gl.FLOAT_VEC3: {
+ return 'uniform3f';
+ }
+ case gl.FLOAT_VEC4: {
+ return 'uniform4f';
+ }
+ case gl.INT: {
+ return 'uniform1i';
+ }
+ case gl.INT_VEC2: {
+ return 'uniform2i';
+ }
+ case gl.INT_VEC3: {
+ return 'uniform3i';
+ }
+ case gl.INT_VEC4: {
+ return 'uniform4i';
+ }
+ case gl.BOOL: {
+ return 'uniform1i';
+ }
+ case gl.BOOL_VEC2: {
+ return 'uniform2i';
+ }
+ case gl.BOOL_VEC3: {
+ return 'uniform3i';
+ }
+ case gl.BOOL_VEC4: {
+ return 'uniform4i';
+ }
+ case gl.FLOAT_MAT2: {
+ return 'uniform1f';
+ }
+ case gl.FLOAT_MAT3: {
+ return 'uniform1f';
+ }
+ case gl.FLOAT_MAT4: {
+ return 'uniform1f';
+ }
+ case gl.SAMPLER_2D: {
+ return 'uniform1f';
+ }
+ case gl.SAMPLER_CUBE: {
+ return 'uniform1f';
+ }
+ case gl.UNSIGNED_INT: {
+ return 'uniform1ui';
+ }
+ case gl.UNSIGNED_INT_VEC2: {
+ return 'uniform2ui';
+ }
+ case gl.UNSIGNED_INT_VEC3: {
+ return 'uniform3ui';
+ }
+ case gl.UNSIGNED_INT_VEC4: {
+ return 'uniform4ui';
+ }
+ case gl.FLOAT_MAT2x3: {
+ return 'uniformMatrix2x3fv';
+ }
+ case gl.FLOAT_MAT2x4: {
+ return 'uniformMatrix2x4fv';
+ }
+ case gl.FLOAT_MAT3x2: {
+ return 'uniformMatrix3x2fv';
+ }
+ case gl.FLOAT_MAT3x4: {
+ return 'uniformMatrix3x4fv';
+ }
+ case gl.FLOAT_MAT4x2: {
+ return 'uniformMatrix4x2fv';
+ }
+ case gl.FLOAT_MAT4x3: {
+ return 'uniformMatrix4x3fv';
+ }
+ case gl.SAMPLER_2D_ARRAY: {
+ return 'uniform1fv';
+ }
+ case gl.SAMPLER_2D_ARRAY_SHADOW: {
+ return 'uniform1f';
+ }
+ case gl.SAMPLER_CUBE_SHADOW: {
+ return 'uniform1f';
+ }
+ case gl.INT_SAMPLER_2D: {
+ return 'uniform1f';
+ }
+ case gl.INT_SAMPLER_3D: {
+ return 'uniform1f';
+ }
+ case gl.INT_SAMPLER_CUBE: {
+ return 'uniform1f';
+ }
+ case gl.INT_SAMPLER_2D_ARRAY: {
+ return 'uniform1f';
+ }
+ case gl.UNSIGNED_INT_SAMPLER_2D: {
+ return 'uniform1ui';
+ }
+ default: {
+ throw new Error(`Unknown uniform type: ${glType}`);
+ }
+ }
+}
type RemoveFirstFromTuple = T['length'] extends 0
? []
@@ -32,7 +180,7 @@ type RemoveFirstFromTuple = T['length'] extends 0
? I
: [];
-type UniformParameters = RemoveFirstFromTuple>;
+type UniformParameters = RemoveFirstFromTuple>;
export interface UniformDefinition {
name: string;
@@ -74,35 +222,96 @@ export interface VertexAttributeDefinition {
export interface ShaderOptions {
/**
- * WebGL2RenderingContext this layout will be attached to, these cannot be reused across webgl contexts.
+ * ExcaliburGraphicsContextWebGL this layout will be attached to, these cannot be reused across webgl contexts.
+ */
+ graphicsContext: ExcaliburGraphicsContext;
+
+ /**
+ * Optionally provide a name for the shader (useful for debugging purposes)
*/
- gl: WebGL2RenderingContext;
+ name?: string;
+
/**
* Vertex shader source code in glsl #version 300 es
*/
vertexSource: string;
+
/**
* Fragment shader source code in glsl #version 300 es
*/
fragmentSource: string;
+ /**
+ * Set initial uniforms
+ */
+ uniforms?: UniformDictionary;
+
+ /**
+ * Set initial images as uniform sampler2D
+ */
+ images?: Record;
+
+ /**
+ * Optionally set the starting texture slot, default 0
+ */
+ startingTextureSlot?: number;
+
+ /**
+ * Callback to fire directly before linking the program
+ */
onPreLink?: (program: WebGLProgram) => void;
+ /**
+ * Callback to fire directly after the progam has finished compiling
+ */
onPostCompile?: (shader: Shader) => void;
}
export class Shader {
+ public readonly name: string = 'anonymous shader';
+ public readonly vertexSource: string;
+ public readonly fragmentSource: string;
+
private static _ACTIVE_SHADER_INSTANCE: Shader | null = null;
+
private _logger = Logger.getInstance();
+
private _gl: WebGL2RenderingContext;
+ private _textureLoader: TextureLoader;
+ private _textures = new Map();
+
public program!: WebGLProgram;
- public uniforms: { [variableName: string]: UniformDefinition } = {};
public attributes: { [variableName: string]: VertexAttributeDefinition } = {};
+
+ private _uniforms: { [variableName: string]: UniformDefinition } = {};
+ private _uniformBuffers: { [blockName: string]: UniformBuffer } = {};
private _compiled = false;
- public readonly vertexSource: string;
- public readonly fragmentSource: string;
private _onPreLink?: (program: WebGLProgram) => void;
private _onPostCompile?: (shader: Shader) => void;
+ private _dirtyUniforms: boolean = true;
+ private _maxTextureSlots: number;
+ private _startingTextureSlot = 0;
+
+ /**
+ * Flags uniforms need to be re-uploaded on the next call to .use()
+ */
+ public flagUniformsDirty(): void {
+ this._dirtyUniforms = true;
+ }
+
+ /**
+ * Set uniforms key value pairs
+ */
+ uniforms: UniformDictionary = watch({}, () => this.flagUniformsDirty());
+
+ /**
+ * Set images to load into the shader as a sampler2d
+ */
+ images: Record = {};
+
+ /**
+ * Returns whether the shader is compiled
+ */
public get compiled() {
return this._compiled;
}
@@ -112,14 +321,34 @@ export class Shader {
* @param options specify shader vertex and fragment source
*/
constructor(options: ShaderOptions) {
- const { gl, vertexSource, fragmentSource, onPreLink, onPostCompile } = options;
- this._gl = gl;
+ const { name, graphicsContext, vertexSource, fragmentSource, onPreLink, onPostCompile, uniforms, images, startingTextureSlot } =
+ options;
+ this.name = name ?? this.name;
+ if (!(graphicsContext instanceof ExcaliburGraphicsContextWebGL)) {
+ throw new Error(`ExcaliburGraphicsContext provided to a shader ${this.name} must be WebGL`);
+ }
+ this._gl = graphicsContext.__gl;
+ this._startingTextureSlot = startingTextureSlot ?? this._startingTextureSlot;
+ this._maxTextureSlots = this._gl.getParameter(this._gl.MAX_TEXTURE_IMAGE_UNITS) - this._startingTextureSlot;
this.vertexSource = vertexSource;
this.fragmentSource = fragmentSource;
+ this.uniforms = watch(uniforms ?? this.uniforms, () => this.flagUniformsDirty());
+ this.images = images ?? this.images;
+ const keys = Object.keys(this.images);
+ if (keys.length >= this._maxTextureSlots) {
+ this._logger.warn(
+ `Max number texture slots ${this._maxTextureSlots} have been reached for material "${this.name}", ` +
+ `no more textures will be uploaded due to hardware constraints.`
+ );
+ }
+ this._textureLoader = graphicsContext.textureLoader;
this._onPreLink = onPreLink;
this._onPostCompile = onPostCompile;
}
+ /**
+ * Deletes the webgl program from the gpu
+ */
dispose() {
const gl = this._gl;
gl.deleteProgram(this.program);
@@ -133,38 +362,156 @@ export class Shader {
const gl = this._gl;
gl.useProgram(this.program);
Shader._ACTIVE_SHADER_INSTANCE = this;
+ if (this._dirtyUniforms) {
+ this._setUniforms();
+ this._dirtyUniforms = false;
+ }
+
+ this._setImages();
+ }
+
+ unuse() {
+ const gl = this._gl;
+ Shader._ACTIVE_SHADER_INSTANCE = null;
+ gl.useProgram(null);
}
isCurrentlyBound() {
return Shader._ACTIVE_SHADER_INSTANCE === this;
}
+ private _setUniforms() {
+ const gl = this._gl;
+ const entries = Object.entries(this.uniforms);
+ if (entries.length) {
+ const uniforms = this.getUniformDefinitions();
+ for (const [key, value] of entries) {
+ if (value instanceof Float32Array) {
+ this.setUniformBuffer(key, value);
+ } else if (Array.isArray(value) && value[0] instanceof Float32Array && typeof value[1] === 'number') {
+ this.setUniformBuffer(key, value[0], value[1]);
+ } else if (typeof value === 'number') {
+ this.trySetUniformFloat(key, value);
+ } else if (typeof value === 'boolean') {
+ this.trySetUniformBoolean(key, value);
+ } else if (value instanceof Vector) {
+ this.trySetUniformFloatVector(key, value);
+ } else if (value instanceof Color) {
+ this.trySetUniformFloatColor(key, value);
+ } else if (value instanceof AffineMatrix) {
+ this.setUniformAffineMatrix(key, value);
+ } else {
+ const uniform = uniforms.find((u) => u.name === key);
+ if (uniform) {
+ const typeName = glTypeToUniformTypeName(gl, uniform.glType) as UniformTypeNames;
+ this.trySetUniform(typeName as any, key, value);
+ } else {
+ this._logger.warnOnce(
+ `Could not locate uniform named ${key},` +
+ ` this can happen if the uniform is unused in the shader code some GPUs will remove this as an optimization.`
+ );
+ }
+ }
+ }
+ }
+ }
+
+ private _loadImageSource(image: ImageSource): WebGLTexture | null {
+ const imageElement = image.image;
+ const maybeFiltering = imageElement.getAttribute(ImageSourceAttributeConstants.Filtering);
+ const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : undefined;
+ const wrapX = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingX) as any);
+ const wrapY = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingY) as any);
+
+ const force = imageElement.getAttribute('forceUpload') === 'true' ? true : false;
+ const texture = this._textureLoader.load(
+ imageElement,
+ {
+ filtering,
+ wrapping: { x: wrapX, y: wrapY }
+ },
+ force
+ );
+ // remove force attribute after upload
+ imageElement.removeAttribute('forceUpload');
+ if (!this._textures.has(image)) {
+ this._textures.set(image, texture!);
+ }
+
+ return texture;
+ }
+
+ _setImages(suppressWarning = false) {
+ const gl = this._gl;
+ // first 2 textures slots are usually taken by 1 default graphic 2 screen texture
+ let textureSlot = this._startingTextureSlot;
+ for (const [textureName, image] of Object.entries(this.images)) {
+ if (!image.isLoaded()) {
+ if (!suppressWarning) {
+ this._logger.warnOnce(
+ `Image named ${textureName} in not loaded, nothing will be uploaded to the shader.` +
+ ` Did you forget to add this to a loader? https://excaliburjs.com/docs/loaders/`
+ );
+ }
+ continue;
+ } // skip unloaded images, maybe warn
+ const texture = this._loadImageSource(image);
+ if (!texture) {
+ if (!suppressWarning) {
+ this._logger.warnOnce(`Image ${textureName} (${image.image.src}) could not be loaded for some reason in shader ${this.name}`);
+ }
+ }
+ gl.activeTexture(gl.TEXTURE0 + textureSlot);
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ this.trySetUniformInt(textureName, textureSlot);
+
+ textureSlot++;
+ }
+ }
+
/**
* Compile the current shader against a webgl context
*/
compile(): WebGLProgram {
+ if (this._compiled) {
+ return this.program;
+ }
const gl = this._gl;
const vertexShader = this._compileShader(gl, this.vertexSource, gl.VERTEX_SHADER);
const fragmentShader = this._compileShader(gl, this.fragmentSource, gl.FRAGMENT_SHADER);
this.program = this._createProgram(gl, vertexShader, fragmentShader);
- const attributes = this.getAttributes();
+ const attributes = this.getAttributeDefinitions();
for (const attribute of attributes) {
this.attributes[attribute.name] = attribute;
}
- const uniforms = this.getUniforms();
+ const uniforms = this.getUniformDefinitions();
for (const uniform of uniforms) {
- this.uniforms[uniform.name] = uniform;
+ this._uniforms[uniform.name] = uniform;
}
this._compiled = true;
if (this._onPostCompile) {
this._onPostCompile(this);
}
+
+ gl.useProgram(this.program);
+ Shader._ACTIVE_SHADER_INSTANCE = this;
+ if (this._dirtyUniforms) {
+ this._setUniforms();
+ this._dirtyUniforms = false;
+ }
+
+ this._setImages(true);
+ this.unuse();
+
return this.program;
}
- getUniforms(): UniformDefinition[] {
+ /**
+ * Get's the uniform definitons
+ */
+ getUniformDefinitions(): UniformDefinition[] {
const gl = this._gl;
const uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
const uniforms: UniformDefinition[] = [];
@@ -180,7 +527,7 @@ export class Shader {
return uniforms;
}
- getAttributes(): VertexAttributeDefinition[] {
+ getAttributeDefinitions(): VertexAttributeDefinition[] {
const gl = this._gl;
const attributeCount = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
const attributes: VertexAttributeDefinition[] = [];
@@ -198,17 +545,86 @@ export class Shader {
return attributes;
}
+ addImageSource(samplerName: string, image: ImageSource) {
+ const keys = Object.keys(this.images);
+ if (keys.length < this._maxTextureSlots) {
+ this.images[samplerName] = image;
+ } else {
+ this._logger.warn(
+ `Max number texture slots ${this._maxTextureSlots} have been reached for material "${this.name}", ` +
+ `no more textures will be uploaded due to hardware constraints.`
+ );
+ }
+ }
+
+ removeImageSource(samplerName: string) {
+ const image = this.images[samplerName];
+ if (image) {
+ this._textureLoader.delete(image.image);
+ delete this.images[samplerName];
+ }
+ }
+
/**
* Set a texture in a gpu texture slot
* @param slotNumber
* @param texture
*/
- setTexture(slotNumber: number, texture: WebGLTexture) {
+ setTexture(slotNumber: number, texture: WebGLTexture): void {
const gl = this._gl;
gl.activeTexture(gl.TEXTURE0 + slotNumber);
gl.bindTexture(gl.TEXTURE_2D, texture);
}
+ /**
+ * Set a uniform buffer block with a Float32Array
+ * @param name The of the binding block
+ * @param data Float32Array
+ * @param [bindingPoint]
+ */
+ setUniformBuffer(name: string, data: Float32Array, bindingPoint: number = 0): void {
+ const gl = this._gl;
+ const index = gl.getUniformBlockIndex(this.program, name);
+ if (index === gl.INVALID_INDEX) {
+ this._logger.warnOnce(`Invalid block name ${name}`);
+ }
+ let uniformBuffer: UniformBuffer;
+ if (this._uniformBuffers[name]) {
+ uniformBuffer = this._uniformBuffers[name];
+ uniformBuffer.bufferData.set(data);
+ uniformBuffer.upload();
+ } else {
+ uniformBuffer = new UniformBuffer({
+ gl,
+ data
+ });
+ this._uniformBuffers[name] = uniformBuffer;
+ }
+ gl.uniformBlockBinding(this.program, index, bindingPoint);
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, uniformBuffer.buffer);
+ }
+
+ trySetUniformBuffer(name: string, data: Float32Array, bindingPoint: number = 0): boolean {
+ if (!this._compiled) {
+ this._logger.warn(`Must compile shader before setting a uniform block ${name} at binding point ${bindingPoint}`);
+ return false;
+ }
+ if (!this.isCurrentlyBound()) {
+ this._logger.warn(
+ 'Currently accessed shader instance is not the current active shader in WebGL,' +
+ ' must call `shader.use()` before setting uniforms'
+ );
+ return false;
+ }
+ const gl = this._gl;
+ const index = gl.getUniformBlockIndex(this.program, name);
+ if (index) {
+ this.setUniformBuffer(name, data, bindingPoint);
+ return true;
+ }
+ return false;
+ }
+
/**
* Set an integer uniform for the current shader
*
@@ -216,7 +632,7 @@ export class Shader {
* @param name
* @param value
*/
- setUniformInt(name: string, value: number) {
+ setUniformInt(name: string, value: number): void {
this.setUniform('uniform1i', name, ~~value);
}
@@ -238,7 +654,7 @@ export class Shader {
* @param name
* @param value
*/
- setUniformIntArray(name: string, value: number[]) {
+ setUniformIntArray(name: string, value: number[]): void {
this.setUniform('uniform1iv', name, value);
}
@@ -260,7 +676,7 @@ export class Shader {
* @param name
* @param value
*/
- setUniformBoolean(name: string, value: boolean) {
+ setUniformBoolean(name: string, value: boolean): void {
this.setUniform('uniform1i', name, value ? 1 : 0);
}
@@ -282,7 +698,7 @@ export class Shader {
* @param name
* @param value
*/
- setUniformFloat(name: string, value: number) {
+ setUniformFloat(name: string, value: number): void {
this.setUniform('uniform1f', name, value);
}
@@ -304,7 +720,7 @@ export class Shader {
* @param name
* @param value
*/
- setUniformFloatArray(name: string, value: number[]) {
+ setUniformFloatArray(name: string, value: number[]): void {
this.setUniform('uniform1fv', name, value);
}
/**
@@ -325,7 +741,7 @@ export class Shader {
* @param name
* @param value
*/
- setUniformFloatVector(name: string, value: Vector) {
+ setUniformFloatVector(name: string, value: Vector): void {
this.setUniform('uniform2f', name, value.x, value.y);
}
@@ -347,7 +763,7 @@ export class Shader {
* @param name
* @param value
*/
- setUniformFloatColor(name: string, value: Color) {
+ setUniformFloatColor(name: string, value: Color): void {
this.setUniform('uniform4f', name, value.r / 255, value.g / 255, value.b / 255, value.a);
}
@@ -369,31 +785,17 @@ export class Shader {
* @param name
* @param value
*/
- setUniformMatrix(name: string, value: Matrix) {
+ setUniformMatrix(name: string, value: Matrix): void {
this.setUniform('uniformMatrix4fv', name, false, value.data);
}
- setUniformAffineMatrix(name: string, value: AffineMatrix) {
+ setUniformAffineMatrix(name: string, value: AffineMatrix): void {
+ // prettier-ignore
this.setUniform('uniformMatrix4fv', name, false, [
- value.data[0],
- value.data[1],
- 0,
- 0,
-
- value.data[2],
- value.data[3],
- 0,
- 0,
-
- 0,
- 0,
- 1,
- 0,
-
- value.data[4],
- value.data[5],
- 0,
- 1
+ value.data[0], value.data[1], 0, 0,
+ value.data[2], value.data[3], 0, 0,
+ 0, 0, 1, 0,
+ value.data[4], value.data[5], 0, 1
]);
}
@@ -413,7 +815,11 @@ export class Shader {
*
* For example setUniform('uniformMatrix2fv', 'u_my2x2_mat`, ...);
*/
- setUniform(uniformType: TUniformType, name: string, ...value: UniformParameters) {
+ setUniform(
+ uniformType: TUniformType,
+ name: string,
+ ...value: UniformParameters
+ ): void {
if (!this._compiled) {
throw Error(`Must compile shader before setting a uniform ${uniformType}:${name}`);
}
diff --git a/src/engine/Graphics/Context/uniform-buffer.ts b/src/engine/Graphics/Context/uniform-buffer.ts
new file mode 100644
index 00000000..26f42ddd
--- /dev/null
+++ b/src/engine/Graphics/Context/uniform-buffer.ts
@@ -0,0 +1,89 @@
+export interface UniformBufferOptions {
+ /**
+ * WebGL2RenderingContext this layout will be attached to, these cannot be reused across contexts.
+ */
+ gl: WebGL2RenderingContext;
+ /**
+ * Size in number of floats, so [4.2, 4.0, 2.1] is size = 3
+ *
+ * **NOTE It is recommended you use multiples of 4's to avoid hardware implemenetation bugs, vec4's the the most supported**
+ *
+ * Ignored if data is passed directly
+ */
+ size?: number;
+ /**
+ * If the vertices never change switching 'static' can be more efficient on the gpu
+ *
+ * Default is 'dynamic'
+ */
+ type?: 'static' | 'dynamic';
+
+ /**
+ * Optionally pass pre-seeded data, size parameter is ignored
+ */
+ data?: Float32Array;
+}
+
+export class UniformBuffer {
+ // TODO provide a struct layout?
+ private _gl: WebGL2RenderingContext;
+ /**
+ * Access to the webgl buffer handle
+ */
+ public readonly buffer: WebGLBuffer;
+ public readonly bufferData: Float32Array;
+ public type: 'static' | 'dynamic' = 'static';
+ private _maxFloats: any;
+ constructor(options: UniformBufferOptions) {
+ const { gl, size, type, data } = options;
+ this._gl = gl;
+ this.buffer = gl.createBuffer()!;
+ this._maxFloats = gl.getParameter(gl.MAX_UNIFORM_BLOCK_SIZE) / 32;
+ if (!data && !size) {
+ throw Error('Must either provide data or a size to the UniformBuffer');
+ }
+
+ if (!data) {
+ this.bufferData = new Float32Array(size!);
+ } else {
+ this.bufferData = data;
+ }
+
+ if (this.bufferData.length > this._maxFloats) {
+ throw Error(`UniformBuffer exceeds browsers allowed number of floats ${this._maxFloats}`);
+ }
+ this.type = type ?? this.type;
+ // Allocate buffer
+ gl.bindBuffer(gl.UNIFORM_BUFFER, this.buffer);
+ gl.bufferData(gl.UNIFORM_BUFFER, this.bufferData, this.type === 'static' ? gl.STATIC_DRAW : gl.DYNAMIC_DRAW);
+ }
+
+ /**
+ * Bind this uniform buffer
+ */
+ bind() {
+ const gl = this._gl;
+ gl.bindBuffer(gl.UNIFORM_BUFFER, this.buffer);
+ }
+
+ unbind() {
+ const gl = this._gl;
+ gl.bindBuffer(gl.UNIFORM_BUFFER, null);
+ }
+
+ upload(count?: number) {
+ const gl = this._gl;
+ gl.bindBuffer(gl.UNIFORM_BUFFER, this.buffer);
+ if (count) {
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.bufferData, 0, count);
+ } else {
+ gl.bufferData(gl.UNIFORM_BUFFER, this.bufferData, this.type === 'static' ? gl.STATIC_DRAW : gl.DYNAMIC_DRAW);
+ }
+ }
+
+ dispose() {
+ const gl = this._gl;
+ gl.deleteBuffer(this.buffer);
+ this._gl = null as any;
+ }
+}
diff --git a/src/engine/Graphics/PostProcessor/ColorBlindnessPostProcessor.ts b/src/engine/Graphics/PostProcessor/ColorBlindnessPostProcessor.ts
index fc746079..e3e7d093 100644
--- a/src/engine/Graphics/PostProcessor/ColorBlindnessPostProcessor.ts
+++ b/src/engine/Graphics/PostProcessor/ColorBlindnessPostProcessor.ts
@@ -4,6 +4,7 @@ import { ColorBlindnessMode } from './ColorBlindnessMode';
import { Shader } from '../Context/shader';
import { VertexLayout } from '../Context/vertex-layout';
import { ScreenShader } from './ScreenShader';
+import { ExcaliburGraphicsContextWebGL } from '../Context/ExcaliburGraphicsContextWebGL';
export class ColorBlindnessPostProcessor implements PostProcessor {
private _shader!: ScreenShader;
@@ -15,8 +16,8 @@ export class ColorBlindnessPostProcessor implements PostProcessor {
this._simulate = simulate;
}
- initialize(gl: WebGL2RenderingContext): void {
- this._shader = new ScreenShader(gl, colorBlindCorrectSource);
+ initialize(graphicsContext: ExcaliburGraphicsContextWebGL): void {
+ this._shader = new ScreenShader(graphicsContext, colorBlindCorrectSource);
this.simulate = this._simulate;
this.colorBlindnessMode = this._colorBlindnessMode;
}
diff --git a/src/engine/Graphics/PostProcessor/PostProcessor.ts b/src/engine/Graphics/PostProcessor/PostProcessor.ts
index de48056d..13fc99f0 100644
--- a/src/engine/Graphics/PostProcessor/PostProcessor.ts
+++ b/src/engine/Graphics/PostProcessor/PostProcessor.ts
@@ -1,5 +1,6 @@
import { VertexLayout } from '../Context/vertex-layout';
import { Shader } from '../Context/shader';
+import { ExcaliburGraphicsContextWebGL } from '../Context/ExcaliburGraphicsContextWebGL';
/**
* PostProcessors can be used to apply a shader to the entire screen. It is recommended
@@ -18,7 +19,7 @@ import { Shader } from '../Context/shader';
* Custom uniforms can be updated in the {@apilink PostProcessor.onUpdate}
*/
export interface PostProcessor {
- initialize(gl: WebGL2RenderingContext): void;
+ initialize(graphicsContext: ExcaliburGraphicsContextWebGL): void;
getShader(): Shader;
getLayout(): VertexLayout;
/**
diff --git a/src/engine/Graphics/PostProcessor/ScreenShader.ts b/src/engine/Graphics/PostProcessor/ScreenShader.ts
index 3cad0121..bff5ed8c 100644
--- a/src/engine/Graphics/PostProcessor/ScreenShader.ts
+++ b/src/engine/Graphics/PostProcessor/ScreenShader.ts
@@ -1,4 +1,5 @@
import { Logger } from '../../Util/Log';
+import { ExcaliburGraphicsContextWebGL } from '../Context/ExcaliburGraphicsContextWebGL';
import { Shader } from '../Context/shader';
import { VertexBuffer } from '../Context/vertex-buffer';
import { VertexLayout } from '../Context/vertex-layout';
@@ -13,7 +14,7 @@ export class ScreenShader {
private _shader: Shader;
private _buffer: VertexBuffer;
private _layout: VertexLayout;
- constructor(gl: WebGL2RenderingContext, fragmentSource: string) {
+ constructor(context: ExcaliburGraphicsContextWebGL, fragmentSource: string) {
if (process.env.NODE_ENV === 'development') {
if (fragmentSource.includes('v_texcoord')) {
Logger.getInstance().warn(
@@ -22,8 +23,9 @@ export class ScreenShader {
);
}
}
+ const gl = context.__gl;
this._shader = new Shader({
- gl,
+ graphicsContext: context,
vertexSource: `#version 300 es
in vec2 a_position;
in vec2 a_uv;
diff --git a/src/engine/Graphics/index.ts b/src/engine/Graphics/index.ts
index 4a99d82f..cebeb172 100644
--- a/src/engine/Graphics/index.ts
+++ b/src/engine/Graphics/index.ts
@@ -51,6 +51,7 @@ export * from './Wrapping';
// Rendering
export * from './Context/shader';
export * from './Context/vertex-buffer';
+export * from './Context/uniform-buffer';
export * from './Context/vertex-layout';
export * from './Context/quad-index-buffer';
export * from './Context/material';
diff --git a/src/spec/MaterialRendererSpec.ts b/src/spec/MaterialRendererSpec.ts
index 8716e23a..1fcbc619 100644
--- a/src/spec/MaterialRendererSpec.ts
+++ b/src/spec/MaterialRendererSpec.ts
@@ -439,7 +439,7 @@ describe('A Material', () => {
expect(material).toBeDefined();
expect(logger.warn).toHaveBeenCalledWith(
- 'Max number texture slots 30 have been reached for material "test", no more textures will be uploaded due to hardware constraints.'
+ 'Max number texture slots 31 have been reached for material "test", no more textures will be uploaded due to hardware constraints.'
);
});
});
diff --git a/src/spec/PostProcessorSpec.ts b/src/spec/PostProcessorSpec.ts
index f7a45473..780a1149 100644
--- a/src/spec/PostProcessorSpec.ts
+++ b/src/spec/PostProcessorSpec.ts
@@ -17,8 +17,8 @@ void main() {
class MockPostProcessor implements ex.PostProcessor {
private _shader: ex.ScreenShader;
- initialize(gl: WebGL2RenderingContext): void {
- this._shader = new ex.ScreenShader(gl, source);
+ initialize(graphicsContext: ex.ExcaliburGraphicsContextWebGL): void {
+ this._shader = new ex.ScreenShader(graphicsContext, source);
}
getShader(): ex.Shader {
return this._shader.getShader();
diff --git a/src/spec/ShaderSpec.ts b/src/spec/ShaderSpec.ts
index 7501e987..06d9aedb 100644
--- a/src/spec/ShaderSpec.ts
+++ b/src/spec/ShaderSpec.ts
@@ -3,16 +3,17 @@ import { ExcaliburMatchers } from 'excalibur-jasmine';
describe('A Shader', () => {
let gl: WebGL2RenderingContext;
+ let graphicsContext: ex.ExcaliburGraphicsContextWebGL;
beforeAll(() => {
jasmine.addMatchers(ExcaliburMatchers);
});
beforeEach(() => {
const canvas = document.createElement('canvas');
- const _ctx = new ex.ExcaliburGraphicsContextWebGL({
+ graphicsContext = new ex.ExcaliburGraphicsContextWebGL({
canvasElement: canvas
});
- gl = _ctx.__gl;
+ gl = graphicsContext.__gl;
});
it('exists', () => {
@@ -21,7 +22,7 @@ describe('A Shader', () => {
it('can be constructed & compiled with shader source', () => {
const sut = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec4 a_position;
attribute vec3 a_otherposition;
@@ -41,30 +42,33 @@ describe('A Shader', () => {
expect(sut.compiled).toBe(false);
sut.compile();
expect(sut.compiled).toBe(true);
- expect(sut.attributes.a_position).toEqual({
+ expect(sut.attributes.a_position).withContext('a_position').toEqual({
name: 'a_position',
glType: gl.FLOAT,
size: 4,
normalized: false,
location: sut.attributes.a_position.location
});
- expect(sut.attributes.a_otherposition).toEqual({
+
+ expect(sut.attributes.a_otherposition).withContext('a_otherposition').toEqual({
name: 'a_otherposition',
glType: gl.FLOAT,
size: 3,
normalized: false,
location: sut.attributes.a_otherposition.location
});
- expect(sut.uniforms.u_bool).toEqual({
+
+ const u_bool = sut.getUniformDefinitions()[0];
+ expect(u_bool).withContext('u_bool').toEqual({
name: 'u_bool',
glType: gl.BOOL,
- location: sut.uniforms.u_bool.location
+ location: u_bool.location
});
});
it('can fail compiling vertex', () => {
const sut = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
uniform int u_int;
// nonsense shader for testing
@@ -85,7 +89,7 @@ describe('A Shader', () => {
it('can fail compiling fragment', () => {
const sut = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
uniform int u_int;
// nonsense shader for testing
@@ -106,7 +110,7 @@ describe('A Shader', () => {
it('must be compiled and shader.use() to set uniforms', () => {
const sut = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
uniform int u_int;
// nonsense shader for testing
@@ -140,7 +144,7 @@ describe('A Shader', () => {
it('can set uniforms', () => {
const sut = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec4 a_position;
attribute vec3 a_otherposition;
@@ -197,7 +201,7 @@ describe('A Shader', () => {
it('can try set uniforms', () => {
const sut = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec4 a_position;
attribute vec3 a_otherposition;
@@ -264,7 +268,7 @@ describe('A Shader', () => {
it('can have textures set', () => {
const sut = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
uniform int u_int;
// nonsense shader for testing
diff --git a/src/spec/VertexLayoutSpec.ts b/src/spec/VertexLayoutSpec.ts
index f94d2e4d..6be5b0e7 100644
--- a/src/spec/VertexLayoutSpec.ts
+++ b/src/spec/VertexLayoutSpec.ts
@@ -3,17 +3,18 @@ import { ExcaliburMatchers } from 'excalibur-jasmine';
describe('A VertexLayout', () => {
let gl: WebGL2RenderingContext;
+ let graphicsContext: ex.ExcaliburGraphicsContextWebGL;
beforeAll(() => {
jasmine.addMatchers(ExcaliburMatchers);
});
beforeEach(() => {
const canvas = document.createElement('canvas');
- const _ctx = new ex.ExcaliburGraphicsContextWebGL({
+ graphicsContext = new ex.ExcaliburGraphicsContextWebGL({
canvasElement: canvas
});
- gl = _ctx.__gl;
+ gl = graphicsContext.__gl;
});
it('exists', () => {
@@ -22,7 +23,7 @@ describe('A VertexLayout', () => {
it('can be constructed', () => {
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
// nonsense shader for testing
void main() {
@@ -52,7 +53,7 @@ describe('A VertexLayout', () => {
it('requires a shader to use', () => {
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec2 a_position;
// nonsense shader for testing
@@ -81,7 +82,7 @@ describe('A VertexLayout', () => {
it('requires a shader to be bound to use', () => {
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec2 a_position;
// nonsense shader for testing
@@ -112,7 +113,7 @@ describe('A VertexLayout', () => {
it('requires a compiled shader', () => {
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec2 a_position;
// nonsense shader for testing
@@ -141,7 +142,7 @@ describe('A VertexLayout', () => {
it('will throw on invalid attribute name', () => {
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec2 a_position;
// nonsense shader for testing
@@ -171,7 +172,7 @@ describe('A VertexLayout', () => {
it('will throw on invalid attribute size', () => {
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec2 a_position;
// nonsense shader for testing
@@ -210,7 +211,7 @@ describe('A VertexLayout', () => {
const enableVertexAttribArraySpy = gl.enableVertexAttribArray as jasmine.Spy;
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec2 a_position;
attribute vec2 a_uv;
@@ -261,7 +262,7 @@ describe('A VertexLayout', () => {
it('can have multiple layouts per shader', () => {
const shader = new ex.Shader({
- gl,
+ graphicsContext,
vertexSource: `
attribute vec2 a_position;
attribute vec2 a_uv;