Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[p5.js 2.0 RFC Proposal]: Shader Hooks #7031

Open
3 of 21 tasks
davepagurek opened this issue May 10, 2024 · 2 comments
Open
3 of 21 tasks

[p5.js 2.0 RFC Proposal]: Shader Hooks #7031

davepagurek opened this issue May 10, 2024 · 2 comments

Comments

@davepagurek
Copy link
Contributor

davepagurek commented May 10, 2024

Increasing access

  • Making shaders easier to learn: currently, from my personal experience teaching and from what I've heard from developers who do not yet know shaders, shaders are one of the hardest things to teach. In order to start, one has to learn a new programming language, learn about multiple phases of a rendering pipeline, learn how to pass data from your sketch to that rendering pipeline, and learn what p5's system uniquely enforces in that system. This is fine for people who have already done shaders outside of p5.js, but leaves a lot to learn at once. Our upcoming intro to shaders article is quite complicated in order to teach all the aspects of shaders (many of which are left out of the current tutorial, which leaves a lot of traps for users to fall into.) If we have an API where we can focus on just one aspect of a shader at a time, it makes it a lot more feasible to learn incrementally without having to do a studying deep dive.
  • Keeping old sketches functioning longer: once one does know how to write shaders, one ends up copy-and-pasting shader code from our default shaders in order to work with existing p5.js functionality. This has the potential to break when we update our internal implementation of the shaders. Having some well-defined hooks goes a long way towards having a stable API (which should also help keep teaching resources up-to-date, helping the first point.)
  • Empowering the addon community: we have had a number of feature requests in the past for updates to the material system. We have not accepted these requests to keep engineering scope and the core API manageable. If we can make a system where developers can make those changes themselves and distribute them to end users, we reduce the pressure on the core library, and provide a new outlet for the creativity that has been stifled by that core library pressure.

Which types of changes would be made?

  • Breaking change (Add-on libraries or sketches will work differently even if their code stays the same.)
  • Systemic change (Many features or contributor workflows will be affected.)
  • Overdue change (Modifications will be made that have been desirable for a long time.)
  • Unsure (The community can help to determine the type of change.)

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)

What's the problem?

tl;dr: if you want to write a shader for p5.js, you have to write the whole thing yourself.

  • This is maybe the hardest part of the library to use
  • It is even harder to do that and also have your shader work with other p5.js features (e.g. transformations and lighting) -- you pretty much have to read the source code and copy relevant parts

What's the solution?

This has been discussed a bit in #6144 (comment), but here's a slightly more refined API:

  • We provide accessors for our default shaders, such as materialShader, normalShader, etc.

  • We add a method to p5.Shader that lets you override different aspects of its behaviour, one function at a time. I'm going to refer to each function as a "hook." An example might be getWorldPosition(), which returns the coordinate of a vertex in world space. If you wanted to modify the default material to make all the vertices wiggle up and down, it would look like this:

    const wiggleShader = materialShader.modify({
      declarations: `uniform float time`,
      getWorldPosition: `(vec3 position) {
        position.y += 10.0 * sin(time * 0.001);
        return position;
      }`
    })

    The modify function takes in an object where entries contain GLSL implementations of whichever hooks you want to override.

    You would then use your modified shader like a normal shader:

    shader(wiggleShader)
    wiggleShader.setUniform('time', millis())
    sphere()

    I've made a prototype with a similar API here as a proof of concept: https://editor.p5js.org/davepagurek/sketches/FAHwrLMAD

  • We update the createShader API to allow creation of these hooks. I'm thinking that will be done by passing an options object as a final parameter, which includes the default implementations of hooks. This API, being for core and library developers, is more verbose than modify: you have to specify which hooks go in the vertex vs the fragment shader, and you have to specify the return type of the functions too.

    In your shader source, you can then assume that the function HOOK_hookName exists. For example:

    const myMaterial = createShader(
      ` // ...preamble goes here
        void main() {
          gl_Position = HOOK_getPosition(vec4(0., 0., 0., 1.));
        }
      `,
      ` // ...fragment shader goes here`,
      {
        vertexHooks: {
          'vec4 getPosition': '(vec4 position) { return position; }'
        }
      }
    )

    Under the hood, modify() will look through the vertex and fragment hooks to see which one matches the (return type free) name. This hopefully frees up some cognitive load from end users.

    Since the shader may want to only call the hook if it exists rather than relying on a function call that does nothing, I'm thinking it'd be useful to also track when a user has called modify and add a #define HOOK_MODIFIED_hookName to the shader too.

  • We create and document hooks for relevant parts of all our default shaders. I've done that for the default material shader in this demo sketch. On the docs for materialShader and the other default shaders, we can then list all the available hooks, and show interesting examples of what you might do with them.

Pros (updated based on community comments)

  • Provides an intro into shaders by focusing on just writing one GLSL function at a time rather than learning the whole rendering pipeline
  • Provides some scaffolding to better document how p5's shaders work (the available hooks help explain the parts that one might be able to modify)
  • Is easier to implement than a full no-GLSL system (think Shader Park's js-to-glsl API)
  • Less boilerplate for a user to jump into than a more flexible shader graph system

Cons (updated based on community comments)

  • This means updating all our shaders to have hooks. I've been starting on this to see what it involves, and it's feasible, but it will definitely cause a bunch of merge conflicts if anyone else is also touching the shaders for anything.
  • This hooks implementation has fundamental limitations of what it can do: unlike a shader graph, there's not a way for library makers to provide e.g. just a noise function that can be dropped into any other shader hook.
  • It still requires users to learn GLSL instead of JavaScript. (However, if we do eventually have a JS api that outputs GLSL, we can still provide that as an alternative to a raw GLSL string when writing hooks.)
  • If we continue to support both WebGL 1 and WebGL 2, we'll probably also have to expose and document something like our GLSL 100+300 compatibility macros. This is yet another thing for users to learn and understand.
  • I've been tinkering on this and it does take a significant amount of time to document every hook in the default shaders.

Proposal status

Under review

@nickmcintyre
Copy link
Member

I'd love to make shaders more accessible to beginners – they're neat. A few questions come to mind:

  • Would these changes complement or impede any other proposed WebGL changes for 2.0?
  • Which approach would be easier for a beginner, writing a shader hook in GLSL or transpiling JavaScript to GLSL?
  • What's a rough estimate for the effort needed to implement shader hooks? How about JS-to-GLSL?
  • Can this feature start as an addon library or does it require significant modifications to the p5.js core?

@davepagurek
Copy link
Contributor Author

davepagurek commented May 27, 2024

Would these changes complement or impede any other proposed WebGL changes for 2.0?

None so far, but I'll keep this updated if anything new comes up.

Which approach would be easier for a beginner, writing a shader hook in GLSL or transpiling JavaScript to GLSL?

I don't think these are mutually exclusive approaches actually. I think the best case scenario is you have both, as they tackle separate bits of complexity. Hooks address the fact that a shader as a whole entity requires a lot of knowledge about the whole shader pipeline and data flow through it, letting you focus on just one piece at a time for less cognitive load. Js-to-glsl addresses the fact that you also need to know a whole separate language in order to write shaders, which is also additional complexity.

I think the hooks problem is the easier one to tackle first for a few reasons. There are a lot of features in GLSL since it's a whole programming language, so there's just a lot of surface area to cover to be able to write anything you'd want to do in GLSL via JavaScript. I don't think you need full coverage for it to be useful, but if you have the option of writing actual GLSL too, then you've got a nice fallback for edge cases outside of what we cover. I imagine in the future, rather than just writing a GLSL string, you'd call some p5 methods that at also support .toString() to turn it to GLSL so that you could swap out anything that's currently a string for a js-to-glsl implementation of some kind. The other current concern is just that it has the potential to add a lot of code, so it might be good to defer that bit to after we've done some initial work on figuring out how to handle optional dependencies to not increase the core bundle size, so that we have some best practices to follow.

One other point I'll mention is that we've put more time thinking about the API for hooks already, and probably the riskiest part was the API planning so that we make something we can support going forward. We haven't done that thinking and de-risking yet for js-to-glsl, so that will add to its development time. Definitely something to start thinking about, but I think probably not actionable yet in the near term for 2.0.

What's a rough estimate for the effort needed to implement shader hooks? How about JS-to-GLSL?

I have a functional prototype with a slightly less refined interface, where it probably is only a day or two of development to get the API to match the proposal. The longer chunk of work, I think, will be documenting all the hooks added into the core shaders. I think it's also OK for that to grow over time, maybe only needing examples for the most useful hooks, and just documenting the types + a description for the other ones at first. I'd like to spend maybe a week getting those high priority ones looking good, and then we could ask for community help on adding to the rest.

For a js-to-glsl project, I think it'll take a bit of R&D at first. There are a lot of different approaches one could take that all have different tradeoffs. A Shader Park style system has you write what looks like a real js function, which it then reads as a source code string, parses, and recompiles. You could also go for a p5.Vector (and p5.warp) like system that makes js function calls to generate objects that track the operations and then generate source code from that. You'd maybe need to look into both more closely and figure out the size/flexibility tradeoffs, and then work from there. That to me sounds like a fairly large project, maybe GSoC sized on the order of months.

Can this feature start as an addon library or does it require significant modifications to the p5.js core?

You could make something like this outside of core p5 and it would work, but it means that the shaders go out of date whenever core p5 changes, and the addon is responsible for copy and pasting source code whenever the p5 source changes. That's currently what p5.warp does, and it means the library breaks slightly with every new version.

One of the reason why I think hooks would be the part to do in 2.0 rather than js-to-glsl is that I think js-to-glsl is maybe a better fit for an addon. As long as core accepts a glsl string, then that's an interchange format between core and the addon that the addon can build for, and since GLSL will not be changing like p5 does, it doesn't have the same problems with going out of date.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Proposal
Development

No branches or pull requests

2 participants