Skip to content

Conversation

@davepagurek
Copy link
Contributor

This partially addresses #7994 and #7992.

The core issue is that a single p5.strands shader involves two callback functions, which is kind of a lot. The outermost one is there because we need to isolate the code that we need to transpile, so that isn't immediately going away (a short term option could be to let people load that from a file rather than a function; a longer term option could be to transpile the whole file via a custom script tag type.) This PR doesn't address that outer one. In this I'm trying to chip away at the inner callback that currently is used for each hook.

Changes:

In general, instead of doing a callback for a hook, you can use begin/end. e.g.:

CallbackFlat
baseMaterialShader().modify(() => {
  let myNormal = sharedVec3()
  
  getPixelInputs((inputs) => {
    myNormal = inputs.normal
    return inputs
  })
  
  getFinalColor.begin((inputs) => {
    return mix(
      [1, 1, 1, 1],
      inputs.color,
      abs(dot(myNormal, [0, 0, 1]))
    )
  })
});
baseMaterialShader().modify(() => {
  let myNormal = sharedVec3()
  
  getPixelInputs.begin()
  myNormal = getPixelInputs.normal
  getPixelInputs.end()
  
  getFinalColor.begin()
  getFinalColor.result = mix(
    [1, 1, 1, 1],
    getFinalColor.color,
    abs(dot(myNormal, [0, 0, 1]))
  );
  getFinalColor.end()
});

Live:
https://editor.p5js.org/davepagurek/sketches/oTsFO63lk

Both forms still work for backwards compatibility.

The rules currently are this:

  • You can replace a callback with a .begin()/.end() block
  • To access the inputs of the hook:
    • If the input is a single object, then you can access its properties on the hook object
    • Otherwise, the name of each argument can be accessed on the hook object
  • To output a value:
    • If the hook takes in and returns the same object type (allowing you to access or modify it), you can just reassign the input properties
    • If the hook returns a new value as output, you can assign to the .result property of the hook

Some other potential thoughts that we could try out:

  • Rather than accessing properties on the hook itself, e.g. getPixelnputs.normal, should we make a global inputs that aliases the hook within its begin/end so you could write inputs.normal?
  • Should we auto-alias get*-prefixed hooks, like getPixelInputs, to pixelInputs so it reads more clearly in this form?
  • With the current rules, since a filter shader's getColor hook has two arguments, inputs and canvasContent, the properties of inputs aren't directly accessible, e.g. you'd have to do getColor.inputs.texCoord rather than just getColor.texCoord. So maybe we'd need to update the rules to give direct access to properties when there's only one object input? (I just don't want name clashes if someone makes a hook that takes two object arguments of the same type.)

PR Checklist

@ksen0
Copy link
Member

ksen0 commented Dec 5, 2025

Thanks for this @davepagurek !

A few thoughts:

If the hook returns a new value as output, you can assign to the .result property of the hook

After fiddling with the sketch a little, I'm wondering if there's a more p5-feeling alternative for assigning .result? Though there's the begin/end pattern in framebuffer, I'm not remembering anything like .result = anywhere.

getFinalColor.begin()
let myNewColor = mix(
  [1, 1, 1, 1],
  getFinalColor.color,
  abs(dot(myNormal, [0, 0, 1]))
);
getFinalColor.set(myNewColor);
getFinalColor.end()

Is one idea, what do you think?

Should we auto-alias get*-prefixed hooks, like getPixelInputs, to pixelInputs so it reads more clearly in this form?

Not sure I understand this, is the code below interpreting the alias idea correctly?

baseMaterialShader().modify(() => {
  let myNormal = sharedVec3()
  
  pixelInputs.begin()
  myNormal = pixelInputs.normal
  pixelInputs.end()
  
  finalColor.begin()
  finalColor.result = mix(
    [1, 1, 1, 1],
    finalColor.color,
    abs(dot(myNormal, [0, 0, 1]))
  );
  finalColor.end()
});

If yes, then I support it, because it more closely matches the begin/end pattern on framebuffer.

Rather than accessing properties on the hook itself, e.g. getPixelnputs.normal, should we make a global inputs that aliases the hook within its begin/end so you could write inputs.normal? ...

Same here, is the code below interpreting the alias idea correctly?

  let myNormal = sharedVec3()
  
  getPixelInputs.begin()
  myNormal = inputs.normal // is this what you meant?
  getPixelInputs.end() // if so, is begin/end here this needed?
  
  getFinalColor.begin()
  getFinalColor.result = mix(
    [1, 1, 1, 1],
    inputs.color, // does this make sense too or no?
    abs(dot(myNormal, [0, 0, 1]))
  );
  getFinalColor.end()
});

Putting it together, it seems much more p5-like:

let myNormal = sharedVec3()

pixelInputs.begin()
myNormal = inputs.normal
pixelInputs.end()

finalColor.begin()

let myNewColor = mix(
  [1, 1, 1, 1],
  inputs.color,
  abs(dot(myNormal, [0, 0, 1]))
);

finalColor.set(myNewColor);
finalColor.end()

@davepagurek
Copy link
Contributor Author

Returning results

I like the .set() idea! I'll take a crack at implementing that later.

Renaming get* prefixes

That is what it would look like, yes!

Global inputs

That's also how it would work, yep! The possible downsides to consider would be:

  • Is inputs too common of a name / would this be clashing with variables users declare?

  • Is there any confusion around reusing of property names across hooks? e.g. currently, it's expected that normal is different in these two contexts, which I like:

    objectInputs.begin()
    // The normal at this stage has not had any transformations applied
    objectInputs.position += objectInputs.normal * 2
    objectInputs.end()
    
    pixelInputs.begin()
    // The normal at this stage has had all transformations applied now
    pixelInputs.color = abs(pixelInputs.normal)
    pixelInputs.end()

    vs with a single global inputs, it may be a bit less clear that they are different values, but the code is a bit simpler:

    objectInputs.begin()
    // The normal at this stage has not had any transformations applied
    inputs.position += inputs.normal * 2
    objectInputs.end()
    
    pixelInputs.begin()
    // The normal at this stage has had all transformations applied now
    inputs.color = abs(inputs.normal)
    pixelInputs.end()

    I guess you'd just have to know that calling .begin() on a hook will update inputs? As far as precedent goes, I think this would be a new pattern. e.g. even though framebuffer.begin() begins capturing drawing output, it doesn't change globals like width and height -- you'd still access and set properties of the framebuffer directly like framebuffer.width.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants