You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
p5.strands: fragment shader that's not filter-oriented? #9326
I was talking to @golanlevin about what it would take to make strands fit in a teaching environment. Currently his course involves providing a fragment shader p5 template that students can fill in, with some examples of doing things like gradients, noise, warping, etc. These are all things that you can do in strands with buildFilterShader, but notably, none of those tasks are filters, so the filter shader API adds some extra conceptual confusion that is not present in vanilla shaders. It seems really feasible that we could make an even simpler version of filter shaders tailored for focused teaching of drawing stuff based on coordinates.
Most appropriate sub-area of p5.js?
Accessibility
Color
Core/Environment/Rendering
Data
DOM
Events
Image
IO
Math
Typography
Utilities
WebGL
Build process
Unit testing
Internationalization
Friendly errors
Other (specify if possible)
Feature enhancement details
Golan refers to these types of full-screen fragment shaders as "fields", both in terms of Rothko color fields and also the idea of vector fields and pixel color fields. So probably we want to remove all verbiage about filters and use something else, "fields" being a good candidate. So the API could be something like:
functionsetup(){createCanvas(400,400,WEBGL)myShader=buildColorFieldShader(gradient)}functiongradient(){field.begin()field.color=[field.texCoord,0,1]// For examplefield.end()}functiondraw(){colorField(myShader)}
Under the hood this could be another base shader, similar to the existing filter shader, with a slightly simpler API (no canvasContent), and a function to run it, colorField(...), which is maybe the same as filter(...) but without the sampling of the underlying canvas.
Reacted by Qianqian Ye and Raphaël de Courville
Reacted by kit, akshat patil, Golan Levin, bhabishnu and Raphaël de Courville
This would be a great fit for teaching. A few things worth settling before implementation:
Naming and hooks
buildColorFieldShader / colorField() / field.* fits Golan's vocabulary. I'd keep field.color and field.texCoord parallel to the filter hooks (filterColor.*) so the two APIs don't drift apart.
Inputs exposed on field
Teaching examples (noise, warping, gradients) almost always need more than normalized texCoord. Worth deciding now whether to also expose pixel coordinates, canvas resolution, and time, so students don't have to declare their own uniforms.
Rendering path
colorField() should draw a full-screen quad independent of camera and transforms.
Should it work from a 2D canvas the way filter() does? The example uses createCanvas(400, 400, WEBGL), so I wasn't sure which is intended.
Target: current canvas only, or also framebuffers / p5.Graphics? filter() already has an answer we can reuse.
Does a field overwrite the canvas, or composite over it (alpha/blend)?
Implementation
Most of this could reuse the filter shader's base shader, hook registration and quad pipeline, minus canvasContent, with its own hook namespace.
I'd be happy to take this on if nobody else is already. I can start by tracing how buildFilterShader and filter() are wired and sketch the minimal diff for a colorField path.
Hey @davepagurek , love this idea. I found a few things about how filter() works. It renders into a separate framebuffer, clears the main canvas and then draws that framebuffer back on top. That only exists because filters have to read the canvas they're writing to. A field doesn't, so colorField() could probably draw straight into the current target. It would also answer the overwrite vs. composite question since it would blend under the current blendMode and you could stack fields with alpha.
The filter vertex shader also goes through the projection and model-view matrices which is why filter() has to reset the camera. A field shader could just output clip-space positions and ignore the camera entirely. Not sure what it should do about depth in a 3D scene though. On inputs, FilterInputs already has canvasSize, so pixel coordinates are just texCoord * canvasSize. Only time is missing from it.
One thing I wasn't sure about: is field.color = ... meant to replace filterColor.set(...)? It looks closer to how worldInputs works. If so, I wonder if field as a global name would clash with student variables.
Thanks @Akshats-git, this matches what I found in the source. filter() only needs the framebuffer round trip and filterCamera reset because it reads the canvas it writes to, so a field can draw straight into the active target and respect blendMode.
New field base vert/frag outputting clip-space positions, with no canvasContent
baseColorFieldShader() alongside the existing baseFilterShader() in the WebGL, 2D (FilterRenderer2D) and WebGPU renderers
buildColorFieldShader mirroring buildFilterShader, and colorField() drawing a quad into the current target
A built-in time input, since FilterInputs doesn't have one
Open questions for you: depth handling in 3D scenes, and whether to keep .set() rather than field.color = ... for consistency with filterColor. Also, a global named field might clash with student variables.
Building on @Akshats-git's point and @LalitNarayanYadav's clip-space approach, I tried a small throwaway version and hit two things that may need handling explicitly.
A fullscreen field can still affect later geometry since normal draws test/write depth, and I saw some WebGL/WebGPU differences around clip-space z.
Also, in WebGL the framebuffer Y-flip currently comes through the camera. Once that's bypassed, texCoord.y flipped between the canvas and a p5.Framebuffer, while WebGPU stayed consistent.
millis(), frameCount, and deltaTime already update inside Strands too, so a separate field time input may not be needed.
@bhabishnu@Akshats-git I don't think we need to consider completely jumping out of the existing camera system; for maintainability, it seems easiest to use the same vertex shader as before, and make running the shader effectively translate to:
push()clearDepth()resetMatrix()camera(defaultCamera)// I forget the name for this but I think we have a camera for filters alreadyshader(theShader)plane(width,height)pop()
This is approximately what filter() already does, but more complicated to capture the existing screen content + also work in 2D mode. So then we don't need any special handling of z, depth, perspective matrices, etc. This also would make it work on framebuffers and graphics without extra work, using the existing syntax for running stuff on those (for framebuffers, you would call it between the framebuffer's begin and end; for graphics, you would call it as a method on the graphics object, which we get for any core add-on that we call once on the p5 prototype and once on the graphics prototype.) Generally I think the approach should be: we have a thing that mostly works, but the focus here is on making a stripped-down API, so we don't need to introduce potential other optimizations and potential bug sources.
From the above, we would get blending, which I agree makes some interesting usage opportunities and is worth keeping.
Should it work from a 2D canvas the way filter() does? The example uses createCanvas(400, 400, WEBGL), so I wasn't sure which is intended.
This is largely a vehicle for teaching GPU programming, which will need WebGL mode anyway, so I think it's OK to just make this for WebGL mode.
millis(), frameCount, and deltaTime already update inside Strands too, so a separate field time input may not be needed.
Originally the filter hook came with more properties because it was designed to be used as a GLSL function, without access to existing p5 globals. Today, though, the API has evolved, and you have access to width and height and frameCount, etc. (Do we support pixelDensity() in strands? That's one thing the current filter API currently has, indirectly, by exposing the texel size. But probably the way to add it here is to add support for pixelDensity() in strands.) So, I agree that we don't need any extra inputs other than the texture coordinate; the rest can come from the other p5 builtins.
One thing I wasn't sure about: is field.color = ... meant to replace filterColor.set(...)? It looks closer to how worldInputs works. If so, I wonder if field as a global name would clash with student variables.
Similar to the above, the set() syntax was kind of never originally planned: at first, every hook was a function, and you would return stuff. But then we switched hooks to primarily use begin/end, which means we can't return without affecting control flow, and so set was introduced in place of return, but also we made it so that hooks with multiple properties let you assign to them directly without having to set. Arguably assigning to a property feels more javascript-native, and we want to slightly move away from set? That also would have future implications for the filterColor hook if we decide we want to align on not using set generally, but we can decide that later I think; for now, the main question to answer is, which feels more natural coming from the rest of p5? I think I slightly lean away from set for that case, but I am not certain enough to just make a decision. I would love some feedback, especially from anyone who teaches with p5 currently.
Thanks @davepagurek , I tried the camera-based version and it does seem to fix the orientation/parity issues I was seeing.
One thing I noticed: with clearDepth() only before the plane, the plane still writes depth and can affect geometry drawn afterwards. Would it make sense to clear again after the plane too, similar to what filter() already does?
That seemed to give the expected result on both WebGL and WebGPU in my test.
8. One thing I noticed: with clearDepth() only before the plane, the plane still writes depth and can affect geometry drawn afterwards. Would it make sense to clear again after the plane too, similar to what filter() already does?
Activity
LalitNarayanYadav commentedon Oct 24, 2026
This would be a great fit for teaching. A few things worth settling before implementation:
Naming and hooks
buildColorFieldShader/colorField()/field.*fits Golan's vocabulary. I'd keepfield.colorandfield.texCoordparallel to the filter hooks (filterColor.*) so the two APIs don't drift apart.Inputs exposed on
fieldtexCoord. Worth deciding now whether to also expose pixel coordinates, canvas resolution, and time, so students don't have to declare their own uniforms.Rendering path
colorField()should draw a full-screen quad independent of camera and transforms.filter()does? The example usescreateCanvas(400, 400, WEBGL), so I wasn't sure which is intended.p5.Graphics?filter()already has an answer we can reuse.Implementation
canvasContent, with its own hook namespace.I'd be happy to take this on if nobody else is already. I can start by tracing how
buildFilterShaderandfilter()are wired and sketch the minimal diff for acolorFieldpath.Akshats-git commentedon Oct 25, 2026
Hey @davepagurek , love this idea. I found a few things about how
filter()works. It renders into a separate framebuffer, clears the main canvas and then draws that framebuffer back on top. That only exists because filters have to read the canvas they're writing to. A field doesn't, socolorField()could probably draw straight into the current target. It would also answer the overwrite vs. composite question since it would blend under the currentblendModeand you could stack fields with alpha.The filter vertex shader also goes through the projection and model-view matrices which is why
filter()has to reset the camera. A field shader could just output clip-space positions and ignore the camera entirely. Not sure what it should do about depth in a 3D scene though. On inputs,FilterInputsalready hascanvasSize, so pixel coordinates are justtexCoord * canvasSize. Only time is missing from it.One thing I wasn't sure about: is
field.color = ...meant to replacefilterColor.set(...)? It looks closer to howworldInputsworks. If so, I wonder iffieldas a global name would clash with student variables.LalitNarayanYadav commentedon Oct 25, 2026
Thanks @Akshats-git, this matches what I found in the source.
filter()only needs the framebuffer round trip andfilterCamerareset because it reads the canvas it writes to, so a field can draw straight into the active target and respectblendMode.I'd like to take this on, @davepagurek. My plan:
fieldbase vert/frag outputting clip-space positions, with nocanvasContentbaseColorFieldShader()alongside the existingbaseFilterShader()in the WebGL, 2D (FilterRenderer2D) and WebGPU renderersbuildColorFieldShadermirroringbuildFilterShader, andcolorField()drawing a quad into the current targettimeinput, sinceFilterInputsdoesn't have oneOpen questions for you: depth handling in 3D scenes, and whether to keep
.set()rather thanfield.color = ...for consistency withfilterColor. Also, a global namedfieldmight clash with student variables.bhabishnu commentedon Oct 26, 2026
Building on @Akshats-git's point and @LalitNarayanYadav's clip-space approach, I tried a small throwaway version and hit two things that may need handling explicitly.
A fullscreen field can still affect later geometry since normal draws test/write depth, and I saw some WebGL/WebGPU differences around clip-space
z.Also, in WebGL the framebuffer Y-flip currently comes through the camera. Once that's bypassed,
texCoord.yflipped between the canvas and ap5.Framebuffer, while WebGPU stayed consistent.millis(),frameCount, anddeltaTimealready update inside Strands too, so a separate fieldtimeinput may not be needed.davepagurek commentedon Oct 26, 2026
@bhabishnu @Akshats-git I don't think we need to consider completely jumping out of the existing camera system; for maintainability, it seems easiest to use the same vertex shader as before, and make running the shader effectively translate to:
This is approximately what
filter()already does, but more complicated to capture the existing screen content + also work in 2D mode. So then we don't need any special handling of z, depth, perspective matrices, etc. This also would make it work on framebuffers and graphics without extra work, using the existing syntax for running stuff on those (for framebuffers, you would call it between the framebuffer'sbeginandend; for graphics, you would call it as a method on the graphics object, which we get for any core add-on that we call once on the p5 prototype and once on the graphics prototype.) Generally I think the approach should be: we have a thing that mostly works, but the focus here is on making a stripped-down API, so we don't need to introduce potential other optimizations and potential bug sources.From the above, we would get blending, which I agree makes some interesting usage opportunities and is worth keeping.
This is largely a vehicle for teaching GPU programming, which will need WebGL mode anyway, so I think it's OK to just make this for WebGL mode.
Originally the filter hook came with more properties because it was designed to be used as a GLSL function, without access to existing p5 globals. Today, though, the API has evolved, and you have access to
widthandheightandframeCount, etc. (Do we supportpixelDensity()in strands? That's one thing the current filter API currently has, indirectly, by exposing the texel size. But probably the way to add it here is to add support forpixelDensity()in strands.) So, I agree that we don't need any extra inputs other than the texture coordinate; the rest can come from the other p5 builtins.Similar to the above, the
set()syntax was kind of never originally planned: at first, every hook was a function, and you wouldreturnstuff. But then we switched hooks to primarily usebegin/end, which means we can'treturnwithout affecting control flow, and sosetwas introduced in place ofreturn, but also we made it so that hooks with multiple properties let you assign to them directly without having toset. Arguably assigning to a property feels more javascript-native, and we want to slightly move away fromset? That also would have future implications for thefilterColorhook if we decide we want to align on not usingsetgenerally, but we can decide that later I think; for now, the main question to answer is, which feels more natural coming from the rest of p5? I think I slightly lean away fromsetfor that case, but I am not certain enough to just make a decision. I would love some feedback, especially from anyone who teaches with p5 currently.bhabishnu commentedon Oct 26, 2026
Thanks @davepagurek , I tried the camera-based version and it does seem to fix the orientation/parity issues I was seeing.
One thing I noticed: with
clearDepth()only before the plane, the plane still writes depth and can affect geometry drawn afterwards. Would it make sense to clear again after the plane too, similar to whatfilter()already does?That seemed to give the expected result on both WebGL and WebGPU in my test.
golanlevin commentedon Oct 26, 2026
Just chiming in to say that I'm very excited about this development! Thanks @davepagurek for championing my suggestion.
davepagurek commentedon Oct 26, 2026
Good catch, I think we should!
davepagurek commentedon Oct 26, 2026
@LalitNarayanYadav I'll assign this to you, let me know though if there's anything else that comes up that needs clarification!