diff --git a/docs/parameterData.json b/docs/parameterData.json
index d8a0c9881f..52f44826c2 100644
--- a/docs/parameterData.json
+++ b/docs/parameterData.json
@@ -1820,6 +1820,21 @@
]
]
},
+ "buildColorFieldShader": {
+ "overloads": [
+ [
+ "Function",
+ "Object?"
+ ]
+ ]
+ },
+ "colorField": {
+ "overloads": [
+ [
+ "p5.Shader"
+ ]
+ ]
+ },
"createSlider": {
"overloads": [
[
diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js
index 280aa31c28..418841d8f6 100644
--- a/src/core/p5.Renderer3D.js
+++ b/src/core/p5.Renderer3D.js
@@ -1160,6 +1160,27 @@ export class Renderer3D extends Renderer {
this.pop();
}
+ colorField(fieldShader) {
+ // Draws into whatever is currently being drawn to (the canvas, or the
+ // active framebuffer). Unlike filter(), nothing is read back from the
+ // target, so no intermediate framebuffer is needed.
+ const target = this.activeFramebuffer() || this;
+ this.push();
+ this.states.setValue('strokeColor', null);
+ // The plane is skipped when there's no fill color (e.g. after noFill())
+ this.fill(0);
+ target.filterCamera._resize();
+ this.setCamera(target.filterCamera);
+ this.resetMatrix();
+ this.clearDepth();
+ this.noLights();
+ this.shader(fieldShader);
+ this.plane(target.width, target.height);
+ // Don't let the full-screen plane affect geometry drawn afterwards
+ this.clearDepth();
+ this.pop();
+ }
+
// Pass this off to the host instance so that we can treat a renderer and a
// framebuffer the same in filter()
diff --git a/src/webgl/material.js b/src/webgl/material.js
index b2f1a3f772..82e569cd53 100644
--- a/src/webgl/material.js
+++ b/src/webgl/material.js
@@ -746,6 +746,95 @@ function material(p5, fn) {
};
p5.registerDecorator('p5.prototype.buildFilterShader', markExperimental('p5.strands', p5));
+ /**
+ * Creates a p5.Shader that colors every pixel of
+ * the canvas based on its coordinates, to be used with
+ * `colorField()`.
+ *
+ * Use it for things like gradients, noise and warping, where each pixel's
+ * color depends on where it is rather than on what was already drawn.
+ * Unlike a filter, a color field doesn't read the existing canvas.
+ *
+ * Pass a function that uses p5.strands. Inside it, call
+ * `fieldColor.begin()`, read the pixel's normalized position from
+ * `fieldColor.texCoord`, pass the resulting color to `fieldColor.set()`,
+ * then call `fieldColor.end()`.
+ *
+ * ```js
+ * let myField;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * myField = buildColorFieldShader(gradient);
+ * }
+ *
+ * function gradient() {
+ * fieldColor.begin();
+ * fieldColor.set([fieldColor.texCoord, 0, 1]);
+ * fieldColor.end();
+ * }
+ *
+ * function draw() {
+ * colorField(myField);
+ * }
+ * ```
+ *
+ * @method buildColorFieldShader
+ * @beta
+ * @submodule p5.strands
+ * @param {Function} callback A function building a p5.strands shader.
+ * @param {Object} [scope] An optional scope object passed to .modify().
+ * @returns {p5.Shader} The color field shader.
+ */
+ fn.buildColorFieldShader = function (callback, scope) {
+ this._assert3d('buildColorFieldShader');
+ return this._renderer.baseColorFieldShader().modify(callback, scope);
+ };
+ p5.registerDecorator('p5.prototype.buildColorFieldShader', markExperimental('p5.strands', p5));
+
+ /**
+ * Fills the whole canvas, or the active
+ * p5.Framebuffer, using a color field shader
+ * made with `buildColorFieldShader()`.
+ *
+ * The field is drawn independently of the camera and any transformations.
+ * It follows the current `blendMode()`, so
+ * fields that use transparency can be layered. Geometry drawn afterwards is
+ * not affected by the field's depth.
+ *
+ * @method colorField
+ * @beta
+ * @param {p5.Shader} fieldShader A shader made with `buildColorFieldShader()`.
+ *
+ * @example
+ *
+ *
+ * let myField;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * myField = buildColorFieldShader(gradient);
+ * describe('A gradient that fades from black at the top left to blue and yellow.');
+ * }
+ *
+ * function gradient() {
+ * fieldColor.begin();
+ * fieldColor.set([fieldColor.texCoord, 0, 1]);
+ * fieldColor.end();
+ * }
+ *
+ * function draw() {
+ * colorField(myField);
+ * }
+ *
+ *
+ */
+ fn.colorField = function (fieldShader) {
+ this._assert3d('colorField');
+ this._renderer.colorField(fieldShader);
+ return this;
+ };
+
/**
* Creates a p5.Shader object to be used with the
* filter() function using GLSL.
diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js
index c3e6d344c9..a91bf9f08c 100644
--- a/src/webgl/p5.RendererGL.js
+++ b/src/webgl/p5.RendererGL.js
@@ -36,6 +36,7 @@ import imageLightVert from './shaders/imageLight.vert';
import imageLightDiffusedFrag from './shaders/imageLightDiffused.frag';
import imageLightSpecularFrag from './shaders/imageLightSpecular.frag';
import filterBaseFrag from './shaders/filters/base.frag';
+import colorFieldBaseFrag from './shaders/field/base.frag';
const { lineDefs } = getStrokeDefs((n, v) => `#define ${n} ${v}\n`);
@@ -55,7 +56,8 @@ const defaultShaders = {
imageLightDiffusedFrag,
imageLightSpecularFrag,
filterBaseVert,
- filterBaseFrag
+ filterBaseFrag,
+ colorFieldBaseFrag
};
for (const key in defaultShaders) {
defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key];
@@ -932,6 +934,32 @@ class RendererGL extends Renderer3D {
return this._baseFilterShader;
}
+ baseColorFieldShader() {
+ if (!this._baseColorFieldShader) {
+ // Reuses the filter vertex shader so that a field is drawn through the
+ // same (default) camera setup as filter()
+ this._baseColorFieldShader = new Shader(
+ this,
+ this._webGL2CompatibilityPrefix('vert', 'highp') +
+ defaultShaders.filterBaseVert,
+ this._webGL2CompatibilityPrefix('frag', 'highp') +
+ defaultShaders.colorFieldBaseFrag,
+ {
+ vertex: {},
+ fragment: {
+ 'vec4 getColor': `(FieldInputs inputs) {
+ return vec4(0.0, 0.0, 0.0, 1.0);
+ }`
+ },
+ hookAliases: {
+ getColor: ['fieldColor']
+ }
+ }
+ );
+ }
+ return this._baseColorFieldShader;
+ }
+
_webGL2CompatibilityPrefix(shaderType, floatPrecision) {
let code = '';
if (this.webglVersion === constants.WEBGL2) {
diff --git a/src/webgl/shaders/field/base.frag b/src/webgl/shaders/field/base.frag
new file mode 100644
index 0000000000..db47fc83b6
--- /dev/null
+++ b/src/webgl/shaders/field/base.frag
@@ -0,0 +1,15 @@
+precision highp float;
+
+IN vec2 vTexCoord;
+
+struct FieldInputs {
+ vec2 texCoord;
+};
+
+void main(void) {
+ FieldInputs inputs;
+ inputs.texCoord = vTexCoord;
+ OUT_COLOR = HOOK_getColor(inputs);
+ // Premultiply alpha so the field blends like other p5 draws
+ OUT_COLOR.rgb *= OUT_COLOR.a;
+}
diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js
index 8e1e1e9c32..515f7073a3 100644
--- a/src/webgpu/p5.RendererWebGPU.js
+++ b/src/webgpu/p5.RendererWebGPU.js
@@ -22,6 +22,10 @@ import {
baseFilterVertexShader,
baseFilterFragmentShader
} from './shaders/filters/base';
+import {
+ baseColorFieldVertexShader,
+ baseColorFieldFragmentShader
+} from './shaders/field/base';
import {
imageLightVertexShader,
imageLightDiffusedFragmentShader,
@@ -5042,6 +5046,28 @@ ${hookUniformFields}}
return this._baseFilterShader;
}
+ baseColorFieldShader() {
+ if (!this._baseColorFieldShader) {
+ this._baseColorFieldShader = new Shader(
+ this,
+ baseColorFieldVertexShader,
+ baseColorFieldFragmentShader,
+ {
+ vertex: {},
+ fragment: {
+ 'vec4 getColor': `(inputs: FieldInputs) -> vec4 {
+ return vec4(0.0, 0.0, 0.0, 1.0);
+ }`
+ },
+ hookAliases: {
+ getColor: ['fieldColor']
+ }
+ }
+ );
+ }
+ return this._baseColorFieldShader;
+ }
+
baseComputeShader() {
if (!this._baseComputeShader) {
this._baseComputeShader = new Shader(this, baseComputeShader, {
diff --git a/src/webgpu/shaders/field/base.js b/src/webgpu/shaders/field/base.js
new file mode 100644
index 0000000000..39e50df13b
--- /dev/null
+++ b/src/webgpu/shaders/field/base.js
@@ -0,0 +1,80 @@
+const fieldUniforms = `
+// Group 0: Field Properties
+struct FieldUniforms {
+ canvasSize: vec2,
+}
+
+// Group 1: Model Transform
+struct ModelUniforms {
+ uModelViewMatrix: mat4x4,
+}
+
+// Group 2: Camera and Projection
+struct CameraUniforms {
+ uProjectionMatrix: mat4x4,
+}
+
+@group(0) @binding(0) var fieldParams: FieldUniforms;
+@group(1) @binding(0) var model: ModelUniforms;
+@group(2) @binding(0) var camera: CameraUniforms;
+`;
+
+export const baseColorFieldVertexShader =
+ fieldUniforms +
+ `
+struct VertexInput {
+ @location(0) aPosition: vec3,
+ @location(1) aTexCoord: vec2,
+}
+
+struct VertexOutput {
+ @builtin(position) position: vec4,
+ @location(0) vTexCoord: vec2,
+}
+
+@vertex
+fn main(input: VertexInput) -> VertexOutput {
+ var output: VertexOutput;
+
+ // transferring texcoords for the frag shader
+ output.vTexCoord = input.aTexCoord;
+
+ // copy position with a fourth coordinate for projection (1.0 is normal)
+ let positionVec4 = vec4(input.aPosition, 1.0);
+
+ // project to 3D space
+ output.position = camera.uProjectionMatrix * model.uModelViewMatrix * positionVec4;
+
+ return output;
+}
+`;
+
+export const baseColorFieldFragmentShader =
+ fieldUniforms +
+ `
+struct FieldInputs {
+ texCoord: vec2,
+}
+
+struct FragmentInput {
+ @location(0) vTexCoord: vec2,
+}
+
+struct FragmentOutput {
+ @location(0) color: vec4,
+}
+
+@fragment
+fn main(input: FragmentInput) -> FragmentOutput {
+ var output: FragmentOutput;
+ var inputs: FieldInputs;
+ inputs.texCoord = input.vTexCoord;
+
+ var outColor = HOOK_getColor(inputs);
+ // Premultiply alpha so the field blends like other p5 draws
+ outColor = vec4(outColor.rgb * outColor.a, outColor.a);
+ output.color = outColor;
+
+ return output;
+}
+`;
diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js
index 96afb6bf12..119ca67882 100644
--- a/test/unit/visual/cases/webgl.js
+++ b/test/unit/visual/cases/webgl.js
@@ -1685,6 +1685,56 @@ visualSuite('WebGL', function () {
);
});
+ visualSuite('colorField', function () {
+ visualTest('On the main canvas', function (p5, screenshot) {
+ p5.createCanvas(50, 50, p5.WEBGL);
+ const shader = p5.buildColorFieldShader(
+ () => {
+ p5.fieldColor.begin();
+ p5.fieldColor.set([p5.fieldColor.texCoord, 0, 1]);
+ p5.fieldColor.end();
+ },
+ { p5 }
+ );
+ p5.colorField(shader);
+ screenshot();
+ });
+
+ visualTest('On a framebuffer', function (p5, screenshot) {
+ p5.createCanvas(50, 50, p5.WEBGL);
+ const shader = p5.buildColorFieldShader(
+ () => {
+ p5.fieldColor.begin();
+ p5.fieldColor.set([p5.fieldColor.texCoord, 0, 1]);
+ p5.fieldColor.end();
+ },
+ { p5 }
+ );
+ const fbo = p5.createFramebuffer();
+ fbo.begin();
+ p5.colorField(shader);
+ fbo.end();
+ p5.imageMode(p5.CENTER);
+ p5.image(fbo, 0, 0);
+ screenshot();
+ });
+
+ visualTest('Blends with transparency', function (p5, screenshot) {
+ p5.createCanvas(50, 50, p5.WEBGL);
+ const shader = p5.buildColorFieldShader(
+ () => {
+ p5.fieldColor.begin();
+ p5.fieldColor.set([1, 0, 0, p5.fieldColor.texCoord.x]);
+ p5.fieldColor.end();
+ },
+ { p5 }
+ );
+ p5.background(0, 0, 255);
+ p5.colorField(shader);
+ screenshot();
+ });
+ });
+
visualSuite('p5.strands', () => {
visualTest('it recovers from p5.strands errors', (p5, screenshot) => {
p5.createCanvas(50, 50, p5.WEBGL);
diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js
index f9f30887f4..22fcc13d37 100644
--- a/test/unit/visual/cases/webgpu.js
+++ b/test/unit/visual/cases/webgpu.js
@@ -265,6 +265,20 @@ visualSuite('WebGPU', function () {
await screenshot();
});
+ visualTest('colorField on the main canvas', async function (p5, screenshot) {
+ await p5.createCanvas(50, 50, p5.WEBGPU);
+ const field = p5.buildColorFieldShader(
+ () => {
+ p5.fieldColor.begin();
+ p5.fieldColor.set([p5.fieldColor.texCoord, 0, 1]);
+ p5.fieldColor.end();
+ },
+ { p5 }
+ );
+ p5.colorField(field);
+ await screenshot();
+ });
+
visualTest('filter shaders with flat API', async function (p5, screenshot) {
await p5.createCanvas(50, 50, p5.WEBGPU);
p5.background(255);
diff --git a/test/unit/visual/screenshots/WebGL/colorField/Blends with transparency/000.png b/test/unit/visual/screenshots/WebGL/colorField/Blends with transparency/000.png
new file mode 100644
index 0000000000..d5b801ee65
Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/colorField/Blends with transparency/000.png differ
diff --git a/test/unit/visual/screenshots/WebGL/colorField/Blends with transparency/metadata.json b/test/unit/visual/screenshots/WebGL/colorField/Blends with transparency/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGL/colorField/Blends with transparency/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGL/colorField/On a framebuffer/000.png b/test/unit/visual/screenshots/WebGL/colorField/On a framebuffer/000.png
new file mode 100644
index 0000000000..460f144e68
Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/colorField/On a framebuffer/000.png differ
diff --git a/test/unit/visual/screenshots/WebGL/colorField/On a framebuffer/metadata.json b/test/unit/visual/screenshots/WebGL/colorField/On a framebuffer/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGL/colorField/On a framebuffer/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGL/colorField/On the main canvas/000.png b/test/unit/visual/screenshots/WebGL/colorField/On the main canvas/000.png
new file mode 100644
index 0000000000..460f144e68
Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/colorField/On the main canvas/000.png differ
diff --git a/test/unit/visual/screenshots/WebGL/colorField/On the main canvas/metadata.json b/test/unit/visual/screenshots/WebGL/colorField/On the main canvas/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGL/colorField/On the main canvas/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/colorField on the main canvas/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/colorField on the main canvas/000.png
new file mode 100644
index 0000000000..460f144e68
Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/Shaders/colorField on the main canvas/000.png differ
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/colorField on the main canvas/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/colorField on the main canvas/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGPU/Shaders/colorField on the main canvas/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js
index c5d5b8d3bf..adcf8404f6 100644
--- a/test/unit/webgl/p5.Shader.js
+++ b/test/unit/webgl/p5.Shader.js
@@ -506,6 +506,55 @@ suite('p5.Shader', function () {
}).not.toThrowError();
});
+ test('buildColorFieldShader colors the canvas from texCoord', () => {
+ myp5.createCanvas(5, 5, myp5.WEBGL);
+ myp5.pixelDensity(1);
+ const myShader = myp5.buildColorFieldShader(() => {
+ fieldColor.begin();
+ fieldColor.set([1, 0, 0, 1]);
+ fieldColor.end();
+ });
+ myp5.background(0);
+ myp5.colorField(myShader);
+ myp5.loadPixels();
+ expect(Array.from(myp5.pixels.slice(0, 4))).toEqual([255, 0, 0, 255]);
+ });
+
+ test('colorField does not leave depth behind for later geometry', () => {
+ myp5.createCanvas(5, 5, myp5.WEBGL);
+ myp5.pixelDensity(1);
+ const myShader = myp5.buildColorFieldShader(() => {
+ fieldColor.begin();
+ fieldColor.set([1, 0, 0, 1]);
+ fieldColor.end();
+ });
+ myp5.background(0);
+ myp5.colorField(myShader);
+ myp5.noStroke();
+ myp5.fill(0, 0, 255);
+ // Slightly behind the field's plane, so it only shows if the field's
+ // depth was cleared afterwards
+ myp5.translate(0, 0, -1);
+ myp5.plane(myp5.width * 2, myp5.height * 2);
+ myp5.loadPixels();
+ expect(Array.from(myp5.pixels.slice(0, 4))).toEqual([0, 0, 255, 255]);
+ });
+
+ test('colorField still draws after noFill()', () => {
+ myp5.createCanvas(5, 5, myp5.WEBGL);
+ myp5.pixelDensity(1);
+ const myShader = myp5.buildColorFieldShader(() => {
+ fieldColor.begin();
+ fieldColor.set([1, 0, 0, 1]);
+ fieldColor.end();
+ });
+ myp5.background(0);
+ myp5.noFill();
+ myp5.colorField(myShader);
+ myp5.loadPixels();
+ expect(Array.from(myp5.pixels.slice(0, 4))).toEqual([255, 0, 0, 255]);
+ });
+
test('accepts an arrow function with an unparenthesized parameter', () => {
myp5.createCanvas(5, 5, myp5.WEBGL);
expect(() => {