Skip to content

Commit 32d4d09

Browse files
committed
Fix clipping for WebGPU
1 parent 2c75c25 commit 32d4d09

7 files changed

Lines changed: 137 additions & 13 deletions

File tree

preview/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696

9797
p.draw = function () {
9898
p.clear();
99+
p.push();
100+
p.clip(() => p.rect(-50, -50, 200, 200));
99101
/*p.orbitControl();
100102
p.push();
101103
p.textAlign(p.CENTER, p.CENTER);
@@ -173,6 +175,7 @@
173175
p.pop();
174176

175177
p.filter(p.BLUR, 10)
178+
p.pop();
176179
};
177180
};
178181

src/core/p5.Renderer3D.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,12 +1341,6 @@ export class Renderer3D extends Renderer {
13411341
this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1]
13421342
) {
13431343
this._clearClip();
1344-
if (!this._userEnabledStencil) {
1345-
this._internalDisable.call(this.GL, this.GL.STENCIL_TEST);
1346-
}
1347-
1348-
// Reset saved state
1349-
// this._userEnabledStencil = this._savedStencilTestState;
13501344
}
13511345
super.pop(...args);
13521346
this._applyStencilTestIfClipping();

src/webgl/p5.RendererGL.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,9 @@ class RendererGL extends Renderer3D {
498498
_clearClipBuffer() {
499499
this.GL.clearStencil(1);
500500
this.GL.clear(this.GL.STENCIL_BUFFER_BIT);
501+
if (!this._userEnabledStencil) {
502+
this._internalDisable.call(this.GL, this.GL.STENCIL_TEST);
503+
}
501504
}
502505

503506
// x,y are canvas-relative (pre-scaled by _pixelDensity)

src/webgpu/p5.RendererWebGPU.js

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,24 +414,22 @@ class RendererWebGPU extends Renderer3D {
414414
multisample: { count: sampleCount },
415415
depthStencil: {
416416
format: depthFormat,
417-
depthWriteEnabled: true,
417+
depthWriteEnabled: !clipping,
418418
depthCompare: 'less-equal',
419419
stencilFront: {
420-
compare: clipping ? 'always' : (clipApplied ? 'equal' : 'always'),
420+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
421421
failOp: 'keep',
422422
depthFailOp: 'keep',
423423
passOp: clipping ? 'replace' : 'keep',
424424
},
425425
stencilBack: {
426-
compare: clipping ? 'always' : (clipApplied ? 'equal' : 'always'),
426+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
427427
failOp: 'keep',
428428
depthFailOp: 'keep',
429429
passOp: clipping ? 'replace' : 'keep',
430430
},
431-
stencilReadMask: clipApplied ? 0xFFFFFFFF : 0x00000000,
432-
stencilWriteMask: clipping ? 0xFFFFFFFF : 0x00000000,
433-
stencilLoadOp: "load",
434-
stencilStoreOp: "store",
431+
stencilReadMask: 0xFF,
432+
stencilWriteMask: clipping ? 0xFF : 0x00,
435433
},
436434
});
437435
shader._pipelineCache.set(key, pipeline);
@@ -1070,6 +1068,18 @@ class RendererWebGPU extends Renderer3D {
10701068
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
10711069
const currentShader = this._curShader;
10721070
passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode })));
1071+
1072+
// Set stencil reference value for clipping
1073+
const drawTarget = this.drawTarget();
1074+
if (drawTarget._isClipApplied && !this._clipping) {
1075+
// When using the clip mask, test against reference value 0 (background)
1076+
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
1077+
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
1078+
passEncoder.setStencilReference(0);
1079+
} else if (this._clipping) {
1080+
// When writing to the clip mask, write reference value 1
1081+
passEncoder.setStencilReference(1);
1082+
}
10731083
// Bind vertex buffers
10741084
for (const buffer of this._getVertexBuffers(currentShader)) {
10751085
const location = currentShader.attributes[buffer.attr].location;
@@ -1569,6 +1579,78 @@ class RendererWebGPU extends Renderer3D {
15691579
return { adjustedWidth: width, adjustedHeight: height };
15701580
}
15711581

1582+
_applyClip() {
1583+
const commandEncoder = this.device.createCommandEncoder();
1584+
1585+
const activeFramebuffer = this.activeFramebuffer();
1586+
const depthTexture = activeFramebuffer ?
1587+
(activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) :
1588+
this.depthTexture;
1589+
1590+
if (!depthTexture) {
1591+
return;
1592+
}
1593+
1594+
const depthStencilAttachment = {
1595+
view: depthTexture.createView(),
1596+
stencilLoadOp: 'clear',
1597+
stencilStoreOp: 'store',
1598+
stencilClearValue: 0,
1599+
depthReadOnly: true,
1600+
stencilReadOnly: false,
1601+
};
1602+
1603+
const renderPassDescriptor = {
1604+
colorAttachments: [],
1605+
depthStencilAttachment: depthStencilAttachment,
1606+
};
1607+
1608+
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
1609+
passEncoder.end();
1610+
1611+
this._pendingCommandEncoders.push(commandEncoder.finish());
1612+
this._hasPendingDraws = true;
1613+
}
1614+
1615+
_unapplyClip() {
1616+
// In WebGPU, clip unapplication is handled through pipeline state rather than direct commands
1617+
// The stencil test configuration is set in the render pipeline based on _clipping and _clipInvert flags
1618+
// This is already handled in the _shaderOptions() method and pipeline creation
1619+
}
1620+
1621+
_clearClipBuffer() {
1622+
const commandEncoder = this.device.createCommandEncoder();
1623+
1624+
const activeFramebuffer = this.activeFramebuffer();
1625+
const depthTexture = activeFramebuffer ?
1626+
(activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) :
1627+
this.depthTexture;
1628+
1629+
if (!depthTexture) {
1630+
return;
1631+
}
1632+
1633+
const depthStencilAttachment = {
1634+
view: depthTexture.createView(),
1635+
stencilLoadOp: 'clear',
1636+
stencilStoreOp: 'store',
1637+
stencilClearValue: 1,
1638+
depthReadOnly: true,
1639+
stencilReadOnly: false,
1640+
};
1641+
1642+
const renderPassDescriptor = {
1643+
colorAttachments: [],
1644+
depthStencilAttachment: depthStencilAttachment,
1645+
};
1646+
1647+
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
1648+
passEncoder.end();
1649+
1650+
this._pendingCommandEncoders.push(commandEncoder.finish());
1651+
this._hasPendingDraws = true;
1652+
}
1653+
15721654
_applyStencilTestIfClipping() {
15731655
// TODO
15741656
}

test/unit/visual/cases/webgpu.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,45 @@ visualSuite("WebGPU", function () {
377377
);
378378
});
379379

380+
visualSuite("Clipping", function () {
381+
visualTest(
382+
"Basic clipping with circles",
383+
async function (p5, screenshot) {
384+
await p5.createCanvas(50, 50, p5.WEBGPU);
385+
p5.background("white");
386+
387+
// Draw some circles that extend beyond the clipping area
388+
p5.fill("red");
389+
p5.noStroke();
390+
p5.circle(-15, -15, 25);
391+
p5.fill("green");
392+
p5.circle(15, -15, 25);
393+
p5.fill("blue");
394+
p5.circle(-15, 15, 25);
395+
p5.fill("yellow");
396+
p5.circle(15, 15, 25);
397+
398+
// Apply clipping to a smaller rectangle in the center
399+
p5.push();
400+
p5.clip(() => {
401+
p5.rect(-12.5, -12.5, 25, 25);
402+
});
403+
404+
// Draw more circles that should be clipped to the rectangle
405+
p5.fill("purple");
406+
p5.circle(-8, -8, 16);
407+
p5.fill("orange");
408+
p5.circle(8, 8, 16);
409+
p5.fill("cyan");
410+
p5.circle(0, 0, 12);
411+
412+
p5.pop();
413+
414+
await screenshot();
415+
},
416+
);
417+
});
418+
380419
visualSuite('Typography', function () {
381420
visualSuite('textFont', function () {
382421
visualTest('with a font file in WebGPU', async function (p5, screenshot) {
1.08 KB
Loading
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"numScreenshots": 1
3+
}

0 commit comments

Comments
 (0)