diff --git a/src/webgl/material.js b/src/webgl/material.js index 594ddecfb3..d7a375d93d 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -523,18 +523,15 @@ function material(p5, fn) { const fragString = await fragSrc.join('\n'); // Test if we've loaded GLSL or not by checking for the existence of `void main` - let loadedShader; - if (/void\s+main/.exec(fragString)) { - loadedShader = this._internal(() => - this.createFilterShader(fragString, true) - ); - } else { - loadedShader = this._internal(() => - withGlobalStrands(this, () => - this.baseFilterShader().modify(new Function(fragString)) - ) - ); - } + const build = /void\s+main/.exec(fragString) + ? () => this.createFilterShader(fragString, true) + : () => + withGlobalStrands(this, () => + this.baseFilterShader().modify(new Function(fragString)) + ); + + // _internal() is provided by FES, which is not included in p5.min.js + let loadedShader = this._internal ? this._internal(build) : build(); if (successCallback) { loadedShader = successCallback(loadedShader) || loadedShader; @@ -1637,9 +1634,9 @@ function material(p5, fn) { fn.loadMaterialShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); - let shader = this._internal(() => - withGlobalStrands(this, () => this.buildMaterialShader(cb)) - ); + const build = () => + withGlobalStrands(this, () => this.buildMaterialShader(cb)); + let shader = this._internal ? this._internal(build) : build(); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -1857,9 +1854,9 @@ function material(p5, fn) { fn.loadNormalShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); - let shader = this._internal(() => - this.withGlobalStrands(this, () => this.buildNormalShader(cb)) - ); + const build = () => + withGlobalStrands(this, () => this.buildNormalShader(cb)); + let shader = this._internal ? this._internal(build) : build(); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -2022,9 +2019,9 @@ function material(p5, fn) { fn.loadColorShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); - let shader = this._internal(() => - withGlobalStrands(this, () => this.buildColorShader(cb)) - ); + const build = () => + withGlobalStrands(this, () => this.buildColorShader(cb)); + let shader = this._internal ? this._internal(build) : build(); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -2285,9 +2282,9 @@ function material(p5, fn) { fn.loadStrokeShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); - let shader = this._internal(() => - withGlobalStrands(this, () => this.buildStrokeShader(cb)) - ); + const build = () => + withGlobalStrands(this, () => this.buildStrokeShader(cb)); + let shader = this._internal ? this._internal(build) : build(); if (onSuccess) { shader = onSuccess(shader) || shader; } diff --git a/test/unit/assets/testColor.js b/test/unit/assets/testColor.js new file mode 100644 index 0000000000..1dcfe5f181 --- /dev/null +++ b/test/unit/assets/testColor.js @@ -0,0 +1,4 @@ +// Test color shader that swaps the red and green channels +getFinalColor(color => { + return [color.g, color.r, color.b, color.a]; +}); diff --git a/test/unit/assets/testNormal.js b/test/unit/assets/testNormal.js new file mode 100644 index 0000000000..02fc7290a8 --- /dev/null +++ b/test/unit/assets/testNormal.js @@ -0,0 +1,5 @@ +// Test normal shader that shifts every vertex to the right +getWorldInputs(inputs => { + inputs.position.x += 10; + return inputs; +}); diff --git a/test/unit/assets/testStroke.js b/test/unit/assets/testStroke.js new file mode 100644 index 0000000000..ec706b7700 --- /dev/null +++ b/test/unit/assets/testStroke.js @@ -0,0 +1,5 @@ +// Test stroke shader that makes strokes three times as thick +getWorldInputs(inputs => { + inputs.weight *= 3; + return inputs; +}); diff --git a/test/unit/io/loadStrandsShaders.js b/test/unit/io/loadStrandsShaders.js new file mode 100644 index 0000000000..dd5bc301c6 --- /dev/null +++ b/test/unit/io/loadStrandsShaders.js @@ -0,0 +1,81 @@ +import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks'; +import { vi } from 'vitest'; +import files from '../../../src/io/files'; +import material from '../../../src/webgl/material'; + +// The mock p5 does not include the Friendly Error System, so these tests also +// cover the minified build, where `_internal()` is not defined. +suite('load*Shader for p5.strands files', function () { + const builtShader = { isBuiltShader: true }; + const stubbed = [ + 'buildMaterialShader', + 'buildNormalShader', + 'buildColorShader', + 'buildStrokeShader', + 'baseFilterShader', + 'createFilterShader' + ]; + const originals = {}; + + beforeAll(async () => { + files(mockP5, mockP5Prototype); + material(mockP5, mockP5Prototype); + await httpMock.start({ quiet: true }); + + // Stub the shader builders so that only the loading logic is tested + for (const name of stubbed) { + originals[name] = mockP5Prototype[name]; + } + mockP5Prototype.buildMaterialShader = vi.fn(() => builtShader); + mockP5Prototype.buildNormalShader = vi.fn(() => builtShader); + mockP5Prototype.buildColorShader = vi.fn(() => builtShader); + mockP5Prototype.buildStrokeShader = vi.fn(() => builtShader); + mockP5Prototype.baseFilterShader = vi.fn(() => ({ + modify: vi.fn(() => builtShader) + })); + mockP5Prototype.createFilterShader = vi.fn(() => builtShader); + }); + + afterAll(() => { + for (const name of stubbed) { + mockP5Prototype[name] = originals[name]; + } + }); + + const loaders = [ + ['loadMaterialShader', 'buildMaterialShader', 'testMaterial.js'], + ['loadNormalShader', 'buildNormalShader', 'testNormal.js'], + ['loadColorShader', 'buildColorShader', 'testColor.js'], + ['loadStrokeShader', 'buildStrokeShader', 'testStroke.js'] + ]; + + for (const [loader, builder, file] of loaders) { + test(`${loader}() returns the shader built from the file`, async () => { + const onFail = vi.fn(); + const shader = await mockP5Prototype[loader]( + `/test/unit/assets/${file}`, + undefined, + onFail + ); + expect(onFail).not.toHaveBeenCalled(); + expect(mockP5Prototype[builder]).toHaveBeenCalledOnce(); + assert.strictEqual(shader, builtShader); + }); + } + + test('loadFilterShader() returns the shader built from a p5.strands file', async () => { + const shader = await mockP5Prototype.loadFilterShader( + '/test/unit/assets/testFilter.js' + ); + expect(mockP5Prototype.baseFilterShader).toHaveBeenCalledOnce(); + assert.strictEqual(shader, builtShader); + }); + + test('loadFilterShader() returns the shader built from a GLSL file', async () => { + const shader = await mockP5Prototype.loadFilterShader( + '/test/unit/assets/frag.glsl' + ); + expect(mockP5Prototype.createFilterShader).toHaveBeenCalledOnce(); + assert.strictEqual(shader, builtShader); + }); +}); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 757e597e47..90c5d636fb 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -743,6 +743,48 @@ visualSuite('WebGL', function () { p5.filter(filterShader); screenshot(); }); + + visualTest('loadNormalShader', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const normalShader = await p5.loadNormalShader( + '/test/unit/assets/testNormal.js' + ); + + // The shader should shift the plane to the right + p5.noStroke(); + p5.shader(normalShader); + p5.plane(20, 20); + screenshot(); + }); + + visualTest('loadColorShader', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const colorShader = await p5.loadColorShader( + '/test/unit/assets/testColor.js' + ); + + // The shader should swap red and green, so the plane appears green + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(colorShader); + p5.plane(20, 20); + screenshot(); + }); + + visualTest('loadStrokeShader', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const strokeShader = await p5.loadStrokeShader( + '/test/unit/assets/testStroke.js' + ); + + // The shader should make the line three times as thick + p5.background(255); + p5.stroke(0); + p5.strokeWeight(4); + p5.strokeShader(strokeShader); + p5.line(-20, 0, 20, 0); + screenshot(); + }); }); visualSuite('Strokes', function () { diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadColorShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadColorShader/000.png new file mode 100644 index 0000000000..873908eaa7 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadColorShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadColorShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadColorShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadColorShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadNormalShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadNormalShader/000.png new file mode 100644 index 0000000000..cbf048726d Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadNormalShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadNormalShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadNormalShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadNormalShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadStrokeShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadStrokeShader/000.png new file mode 100644 index 0000000000..62778803d0 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadStrokeShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadStrokeShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadStrokeShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadStrokeShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file