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

Updated Stencil Test Case to prevent it from disable after every draw #7578

Open
wants to merge 7 commits into
base: dev-2.0
Choose a base branch
from

Conversation

Vaivaswat2244
Copy link

Resolves #7554

Changes:

  • Introduced this.isStencilTestOn and this._userEnabledStencil to track stencil state.
  • Overrode WebGL's enable(), disable(), and getEnabled() methods to ensure:
    • The stencil test is marked as user-enabled only when explicitly set outside clip().
    • pop() restores the correct stencil state after clipping operations.
  • Modified stencil test logic:
    • Prevent unnecessary disable(STENCIL_TEST) calls when it is user-enabled.
    • Restore user-enabled stencil test state after pop().

Screenshots of the change:

Used this snippet in preview/index.html,

<body>
 <script type="module">
   import p5 from '../src/app.js';

   const sketch = function (p) {
     let gl;

     p.setup = function () {
       p.createCanvas(400, 400, p.WEBGL);
       p.noStroke();
       gl = p._renderer.GL;
       gl.enable(gl.STENCIL_TEST);
     };

     p.draw = function () {
       p.background(0);
       gl.clear(gl.STENCIL_BUFFER_BIT);

       // Set stencil value to 1
       gl.stencilFunc(gl.ALWAYS, 1, ~0);
       gl.stencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE);

       p.fill(255, 0, 0);
       p.plane(200, 400);

       // Only draw where stencil value is 1
       gl.stencilFunc(gl.EQUAL, 1, ~0);
       gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);

       p.fill(0, 0, 255);
       p.plane(400, 200);

       p.noLoop();
     };
   };

   new p5(sketch);
 </script>
</body>
</html>

SCREENSHOT:
Screenshot from 2025-02-26 19-30-52

PR Checklist

// When pop() disables the stencil test after clip(),
// restore the user's stencil test setting
if (this._clipDepth === this._pushPopDepth) {
this.isStencilTestOn = this._userEnabledStencil;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little unintuitive that calling disable() elsewhere in the code might not actually disable. How do you feel about making enable/disable only record the last value so that getEnabled is fast, and then place this logic of what value to revert to elsewhere?

The idea would be, write the code so that the behaviour would be the same even if we haven't overridden enable and disable: e.g. when we begin clipping, we can record this.isStencilTestOn = this.drawingContext.getEnabled(this.drawingContext.STENCIL_TEST), and then in pop where we turn off stencil testing currently, we would either turn it on or off based on the value of this.isStencilTestOn.

Then, this overriding of enable/disable just makes the getEnabled call run faster, without changing what it does or its side effects.

@davepagurek
Copy link
Contributor

Thanks for taking this task on, it's looking good! I have some comments on how we might rearrange the implementation to make the code easier for other contributors to read.

@davepagurek
Copy link
Contributor

One other thought: if stencil testing was on, after we finish clipping in pop, do we want to clear the stencil buffer? otherwise it effectively just continues clipping.

@Vaivaswat2244
Copy link
Author

One other thought: if stencil testing was on, after we finish clipping in pop, do we want to clear the stencil buffer? otherwise it effectively just continues clipping.

So, right now, I'm clearing the stencil buffer at the beginning of beginClip() but not when exiting the clip context in pop().
clearing the buffer while exiting makes sense.
I can add gl.clear(gl.STENCIL_BUFFER_BIT) in the pop() function after restoring the previous stencil test state to ensure a clean slate for future operations.
Would you recommend this, or maybe there are cases where preserving the stencil buffer contents across pop operations would be beneficial?

Copy link
Contributor

@davepagurek davepagurek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts on the overall logic

Just looking again at the if statements in the drawingContext.enable override: Is the if statement in here trying to detect if the user is manually calling drawingContext.disable vs if p5 itself is doing it?

One idea to try to simplify this: Maybe we should take those prevEnable and prevDisable variable we have, and store it on the renderer with names like internalEnable and internalDisable. Calling those just enable/disable; and then we're free to make our overrides a lot simpler, just:

this.drawingContext.enable = (key) => {
  if (key === this.drawingContext.STENCIL_TEST) {
    this._userEnabledStencil = true
  }
  return prevEnable.call(key)

Then:

  • wherever we internally call GL.enable(GL.STENCIL_TEST), we'd call internalEnable(GL.STENCIL_TEST) instead so that it keeps _userEnabledStencil the same
  • in all the places where we currently just disable stencil testing, we'd only actually disable it if _userEnabledStencil is false too, and otherwise leave it on.

This way we also don't need to record what the value is right before we enable stencil testing, since we don't really need to know what it was right before if we've recorded what the user has set it to.

I think that would mean we don't need the isStencilTestOn check as well, and might simplify some of our logic. Do you think that would work?

Clearing the buffer on pop

Thinking a little more about the use case here, clearing the stencil buffer in pop would only make a difference if the user already manually enabled the stencil buffer, and additionally is using p5 clipping. I think that's actually not a combination we need to worry about, since the stencil buffer does clipping, if you're turning it on manually, the user won't be using p5's clipping. So maybe we take the simpler approach and don't do anything additional here.

Testing

Sorry for all the back-and-forth on this, it turns out the logic is complicated haha. That's maybe a good sign that we should make some unit tests involving manual calls to enable/disable the stencil test?

@@ -1750,7 +1809,9 @@ class RendererGL extends Renderer {
this.GL.enable(this.GL.STENCIL_TEST);
this._stencilTestOn = true;
} else {
this.GL.disable(this.GL.STENCIL_TEST);
if (!this.isStencilTestOn) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this only disable if the user has not manually called disable? If so, maybe we need to be using the _userEnabledStencil for the if condition?

@Vaivaswat2244
Copy link
Author

Some thoughts on the overall logic

Just looking again at the if statements in the drawingContext.enable override: Is the if statement in here trying to detect if the user is manually calling drawingContext.disable vs if p5 itself is doing it?

One idea to try to simplify this: Maybe we should take those prevEnable and prevDisable variable we have, and store it on the renderer with names like internalEnable and internalDisable. Calling those just enable/disable; and then we're free to make our overrides a lot simpler, just:

this.drawingContext.enable = (key) => {
  if (key === this.drawingContext.STENCIL_TEST) {
    this._userEnabledStencil = true
  }
  return prevEnable.call(key)

Then:

  • wherever we internally call GL.enable(GL.STENCIL_TEST), we'd call internalEnable(GL.STENCIL_TEST) instead so that it keeps _userEnabledStencil the same
  • in all the places where we currently just disable stencil testing, we'd only actually disable it if _userEnabledStencil is false too, and otherwise leave it on.

This way we also don't need to record what the value is right before we enable stencil testing, since we don't really need to know what it was right before if we've recorded what the user has set it to.

I think that would mean we don't need the isStencilTestOn check as well, and might simplify some of our logic. Do you think that would work?

Clearing the buffer on pop

Thinking a little more about the use case here, clearing the stencil buffer in pop would only make a difference if the user already manually enabled the stencil buffer, and additionally is using p5 clipping. I think that's actually not a combination we need to worry about, since the stencil buffer does clipping, if you're turning it on manually, the user won't be using p5's clipping. So maybe we take the simpler approach and don't do anything additional here.

Testing

Sorry for all the back-and-forth on this, it turns out the logic is complicated haha. That's maybe a good sign that we should make some unit tests involving manual calls to enable/disable the stencil test?

Sure, got it.
I have made the changes required. Tests are not included rn, once you confirm these I can write tests involving manual calls to enable/disable the stencil test.

Copy link
Contributor

@davepagurek davepagurek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updates are looking good! I think the general approach looks almost complete. There are a few cases being handled that I think we may not need to handle any more and can be omitted. But in general this looks almost done, feel free to start adding tests!

@@ -1387,7 +1423,7 @@ class RendererGL extends Renderer {
this.drawTarget()._isClipApplied = true;

const gl = this.GL;
gl.clearStencil(0);
gl.clearStencil(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a trailing space at the end here that we can take out.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

if (key === this.drawingContext.STENCIL_TEST) {
// When pop() disables the stencil test after clip(),
// preserve the user's stencil test setting
if (this._clipDepth === this._pushPopDepth) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this condition here, since we're calling _internalDisable in pop? I think maybe this is only called now when users manually call disable, so we can set this._userEnabledStencil = true. The same may be true in enable as well if we update the spots where we turn on stencil testing to use _internalEnable

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! I'll do this for enable as well.

@@ -427,6 +427,39 @@ class RendererGL extends Renderer {
this.drawShapeCount = 1;

this.scratchMat3 = new Matrix(3);

this._userEnabledStencil = false;
this.isStencilTestOn = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use this any more?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we don't.

@Vaivaswat2244
Copy link
Author

So, I have added the tests for stencil testing which I thought were appropriate. Let me know to add or remove some cases. Also replaced the internalEnable for enable in the file.

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.

2 participants