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;