Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/parameterData.json
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,21 @@
]
]
},
"buildColorFieldShader": {
"overloads": [
[
"Function",
"Object?"
]
]
},
"colorField": {
"overloads": [
[
"p5.Shader"
]
]
},
"createSlider": {
"overloads": [
[
Expand Down
21 changes: 21 additions & 0 deletions src/core/p5.Renderer3D.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
89 changes: 89 additions & 0 deletions src/webgl/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,95 @@ function material(p5, fn) {
};
p5.registerDecorator('p5.prototype.buildFilterShader', markExperimental('p5.strands', p5));

/**
* Creates a <a href="#/p5.Shader">p5.Shader</a> that colors every pixel of
* the canvas based on its coordinates, to be used with
* <a href="#/p5/colorField">`colorField()`</a>.
*
* 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
* <a href="#/p5.Framebuffer">p5.Framebuffer</a>, using a color field shader
* made with <a href="#/p5/buildColorFieldShader">`buildColorFieldShader()`</a>.
*
* The field is drawn independently of the camera and any transformations.
* It follows the current <a href="#/p5/blendMode">`blendMode()`</a>, 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
* <div>
* <code>
* 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);
* }
* </code>
* </div>
*/
fn.colorField = function (fieldShader) {
this._assert3d('colorField');
this._renderer.colorField(fieldShader);
return this;
};

/**
* Creates a <a href="#/p5.Shader">p5.Shader</a> object to be used with the
* <a href="#/p5/filter">filter()</a> function using GLSL.
Expand Down
30 changes: 29 additions & 1 deletion src/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);

Expand All @@ -55,7 +56,8 @@ const defaultShaders = {
imageLightDiffusedFrag,
imageLightSpecularFrag,
filterBaseVert,
filterBaseFrag
filterBaseFrag,
colorFieldBaseFrag
};
for (const key in defaultShaders) {
defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key];
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions src/webgl/shaders/field/base.frag
Original file line number Diff line number Diff line change
@@ -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;
}
26 changes: 26 additions & 0 deletions src/webgpu/p5.RendererWebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import {
baseFilterVertexShader,
baseFilterFragmentShader
} from './shaders/filters/base';
import {
baseColorFieldVertexShader,
baseColorFieldFragmentShader
} from './shaders/field/base';
import {
imageLightVertexShader,
imageLightDiffusedFragmentShader,
Expand Down Expand Up @@ -5042,6 +5046,28 @@ ${hookUniformFields}}
return this._baseFilterShader;
}

baseColorFieldShader() {
if (!this._baseColorFieldShader) {
this._baseColorFieldShader = new Shader(
this,
baseColorFieldVertexShader,
baseColorFieldFragmentShader,
{
vertex: {},
fragment: {
'vec4<f32> getColor': `(inputs: FieldInputs) -> vec4<f32> {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}`
},
hookAliases: {
getColor: ['fieldColor']
}
}
);
}
return this._baseColorFieldShader;
}

baseComputeShader() {
if (!this._baseComputeShader) {
this._baseComputeShader = new Shader(this, baseComputeShader, {
Expand Down
80 changes: 80 additions & 0 deletions src/webgpu/shaders/field/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const fieldUniforms = `
// Group 0: Field Properties
struct FieldUniforms {
canvasSize: vec2<f32>,
}

// Group 1: Model Transform
struct ModelUniforms {
uModelViewMatrix: mat4x4<f32>,
}

// Group 2: Camera and Projection
struct CameraUniforms {
uProjectionMatrix: mat4x4<f32>,
}

@group(0) @binding(0) var<uniform> fieldParams: FieldUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> camera: CameraUniforms;
`;

export const baseColorFieldVertexShader =
fieldUniforms +
`
struct VertexInput {
@location(0) aPosition: vec3<f32>,
@location(1) aTexCoord: vec2<f32>,
}

struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) vTexCoord: vec2<f32>,
}

@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<f32>(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<f32>,
}

struct FragmentInput {
@location(0) vTexCoord: vec2<f32>,
}

struct FragmentOutput {
@location(0) color: vec4<f32>,
}

@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<f32>(outColor.rgb * outColor.a, outColor.a);
output.color = outColor;

return output;
}
`;
50 changes: 50 additions & 0 deletions test/unit/visual/cases/webgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading