Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/materials/Material.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,53 @@ class Material extends EventDispatcher {
*/
this.stencilWrite = false;

/**
* The stencil comparison function for back faces, or `null` to use
* {@link Material#stencilFunc} for both faces.
*
* @type {?number}
* @default null
*/
this.stencilBackFunc = null;

/**
* The stencil reference value for back faces, or `null` to use
* {@link Material#stencilRef} for both faces.
Copy link
Copy Markdown
Collaborator

@Mugen87 Mugen87 Apr 2, 2026

Choose a reason for hiding this comment

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

stencilBackRef is ignored in WebGPU. WebGPU only offers GPURenderPassEncoder.setStencilReference() that means you can only set a single stencil reference for front and back faces.

We could a) remove stencilBackRef or b) just document that it is WebGL 2 only. That might cause rendering issue though. A feature that works in WebGL 2 won't work in WebGPU.

I vote doing the same with masks and adhere to the WebGPU spec. What do you think?

*
* @type {?number}
* @default null
*/
this.stencilBackRef = null;

/**
* Which stencil operation to perform on back faces when the comparison
* function returns `false`, or `null` to use {@link Material#stencilFail}.
*
* @type {?number}
* @default null
*/
this.stencilBackFail = null;

/**
* Which stencil operation to perform on back faces when the comparison
* function returns `true` but the depth test fails, or `null` to use
* {@link Material#stencilZFail}.
*
* @type {?number}
* @default null
*/
this.stencilBackZFail = null;

/**
* Which stencil operation to perform on back faces when the comparison
* function returns `true` and the depth test passes, or `null` to use
* {@link Material#stencilZPass}.
*
* @type {?number}
* @default null
*/
this.stencilBackZPass = null;

/**
* User-defined clipping planes specified as THREE.Plane objects in world
* space. These planes apply to the objects this material is attached to.
Expand Down Expand Up @@ -813,6 +860,12 @@ class Material extends EventDispatcher {
if ( this.stencilZPass !== KeepStencilOp ) data.stencilZPass = this.stencilZPass;
if ( this.stencilWrite === true ) data.stencilWrite = this.stencilWrite;

if ( this.stencilBackFunc !== null ) data.stencilBackFunc = this.stencilBackFunc;
if ( this.stencilBackRef !== null ) data.stencilBackRef = this.stencilBackRef;
if ( this.stencilBackFail !== null ) data.stencilBackFail = this.stencilBackFail;
if ( this.stencilBackZFail !== null ) data.stencilBackZFail = this.stencilBackZFail;
if ( this.stencilBackZPass !== null ) data.stencilBackZPass = this.stencilBackZPass;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MaterialLoader also needs an update so the deserialization works:

if ( json.stencilFunc !== undefined ) material.stencilFunc = json.stencilFunc;
if ( json.stencilRef !== undefined ) material.stencilRef = json.stencilRef;
if ( json.stencilFuncMask !== undefined ) material.stencilFuncMask = json.stencilFuncMask;
if ( json.stencilFail !== undefined ) material.stencilFail = json.stencilFail;
if ( json.stencilZFail !== undefined ) material.stencilZFail = json.stencilZFail;
if ( json.stencilZPass !== undefined ) material.stencilZPass = json.stencilZPass;
if ( json.stencilWrite !== undefined ) material.stencilWrite = json.stencilWrite;

// rotation (SpriteMaterial)
if ( this.rotation !== undefined && this.rotation !== 0 ) data.rotation = this.rotation;

Expand Down Expand Up @@ -931,6 +984,12 @@ class Material extends EventDispatcher {
this.stencilZPass = source.stencilZPass;
this.stencilWrite = source.stencilWrite;

this.stencilBackFunc = source.stencilBackFunc;
this.stencilBackRef = source.stencilBackRef;
this.stencilBackFail = source.stencilBackFail;
this.stencilBackZFail = source.stencilBackZFail;
this.stencilBackZPass = source.stencilBackZPass;

const srcPlanes = source.clippingPlanes;
let dstPlanes = null;

Expand Down
133 changes: 127 additions & 6 deletions src/renderers/webgl-fallback/utils/WebGLState.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,21 @@ class WebGLState {
this.currentDepthReversed = false;
this.currentDepthFunc = null;
this.currentDepthMask = null;
this.currentStencilSeparate = false;
this.currentStencilFunc = null;
this.currentStencilRef = null;
this.currentStencilFuncMask = null;
this.currentStencilFuncBack = null;
this.currentStencilRefBack = null;
this.currentStencilFuncMaskBack = null;
this.currentStencilFail = null;
this.currentStencilZFail = null;
this.currentStencilZPass = null;
this.currentStencilFailBack = null;
this.currentStencilZFailBack = null;
this.currentStencilZPassBack = null;
this.currentStencilMask = null;
this.currentStencilMaskBack = null;
this.currentLineWidth = null;
this.currentClippingPlanes = 0;

Expand Down Expand Up @@ -829,10 +837,30 @@ class WebGLState {
*/
setStencilMask( stencilMask ) {

if ( this.currentStencilMask !== stencilMask ) {
if ( this.currentStencilSeparate === true || this.currentStencilMask !== stencilMask ) {
Copy link
Copy Markdown
Collaborator

@Mugen87 Mugen87 Apr 2, 2026

Choose a reason for hiding this comment

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

I'm not sure these checks are robust enough:

If you have used separate mask and then switch to a uniform mask, gl.stencilMask() might not be executed if this.currentStencilMask is equal to stencilMask. Meaning it would leave the back faces with a wrong value (the one from the separate call).

I think you must implement a check that reconginizes when currentStencilSeparate changes. If it does, it must invalidate the cached properties so the WebGL state is forced to update.

Func and Op are also affected by this issue.


this.gl.stencilMask( stencilMask );
this.currentStencilMask = stencilMask;
this.currentStencilMaskBack = stencilMask;
this.currentStencilSeparate = false;

}

}

setStencilMaskSeparate( stencilMask, stencilMaskBack ) {

if (
this.currentStencilSeparate === false ||
this.currentStencilMask !== stencilMask ||
this.currentStencilMaskBack !== stencilMaskBack
) {

this.gl.stencilMaskSeparate( this.gl.FRONT, stencilMask );
this.gl.stencilMaskSeparate( this.gl.BACK, stencilMaskBack );
this.currentStencilMask = stencilMask;
this.currentStencilMaskBack = stencilMaskBack;
this.currentStencilSeparate = true;

}

Expand All @@ -850,7 +878,8 @@ class WebGLState {
*/
setStencilFunc( stencilFunc, stencilRef, stencilMask ) {

if ( this.currentStencilFunc !== stencilFunc ||
if ( this.currentStencilSeparate === true ||
this.currentStencilFunc !== stencilFunc ||
this.currentStencilRef !== stencilRef ||
this.currentStencilFuncMask !== stencilMask ) {

Expand All @@ -859,6 +888,37 @@ class WebGLState {
this.currentStencilFunc = stencilFunc;
this.currentStencilRef = stencilRef;
this.currentStencilFuncMask = stencilMask;
this.currentStencilFuncBack = stencilFunc;
this.currentStencilRefBack = stencilRef;
this.currentStencilFuncMaskBack = stencilMask;
this.currentStencilSeparate = false;

}

}

setStencilFuncSeparate( stencilFunc, stencilRef, stencilMask, stencilFuncBack, stencilRefBack, stencilMaskBack ) {

if (
this.currentStencilSeparate === false ||
this.currentStencilFunc !== stencilFunc ||
this.currentStencilRef !== stencilRef ||
this.currentStencilFuncMask !== stencilMask ||
this.currentStencilFuncBack !== stencilFuncBack ||
this.currentStencilRefBack !== stencilRefBack ||
this.currentStencilFuncMaskBack !== stencilMaskBack
) {

this.gl.stencilFuncSeparate( this.gl.FRONT, stencilFunc, stencilRef, stencilMask );
this.gl.stencilFuncSeparate( this.gl.BACK, stencilFuncBack, stencilRefBack, stencilMaskBack );

this.currentStencilFunc = stencilFunc;
this.currentStencilRef = stencilRef;
this.currentStencilFuncMask = stencilMask;
this.currentStencilFuncBack = stencilFuncBack;
this.currentStencilRefBack = stencilRefBack;
this.currentStencilFuncMaskBack = stencilMaskBack;
this.currentStencilSeparate = true;

}

Expand All @@ -877,7 +937,8 @@ class WebGLState {
*/
setStencilOp( stencilFail, stencilZFail, stencilZPass ) {

if ( this.currentStencilFail !== stencilFail ||
if ( this.currentStencilSeparate === true ||
this.currentStencilFail !== stencilFail ||
this.currentStencilZFail !== stencilZFail ||
this.currentStencilZPass !== stencilZPass ) {

Expand All @@ -886,6 +947,37 @@ class WebGLState {
this.currentStencilFail = stencilFail;
this.currentStencilZFail = stencilZFail;
this.currentStencilZPass = stencilZPass;
this.currentStencilFailBack = stencilFail;
this.currentStencilZFailBack = stencilZFail;
this.currentStencilZPassBack = stencilZPass;
this.currentStencilSeparate = false;

}

}

setStencilOpSeparate( stencilFail, stencilZFail, stencilZPass, stencilFailBack, stencilZFailBack, stencilZPassBack ) {

if (
this.currentStencilSeparate === false ||
this.currentStencilFail !== stencilFail ||
this.currentStencilZFail !== stencilZFail ||
this.currentStencilZPass !== stencilZPass ||
this.currentStencilFailBack !== stencilFailBack ||
this.currentStencilZFailBack !== stencilZFailBack ||
this.currentStencilZPassBack !== stencilZPassBack
) {

this.gl.stencilOpSeparate( this.gl.FRONT, stencilFail, stencilZFail, stencilZPass );
this.gl.stencilOpSeparate( this.gl.BACK, stencilFailBack, stencilZFailBack, stencilZPassBack );

this.currentStencilFail = stencilFail;
this.currentStencilZFail = stencilZFail;
this.currentStencilZPass = stencilZPass;
this.currentStencilFailBack = stencilFailBack;
this.currentStencilZFailBack = stencilZFailBack;
this.currentStencilZPassBack = stencilZPassBack;
this.currentStencilSeparate = true;

}

Expand Down Expand Up @@ -924,9 +1016,38 @@ class WebGLState {
this.setStencilTest( stencilWrite );
if ( stencilWrite ) {

this.setStencilMask( material.stencilWriteMask );
this.setStencilFunc( material.stencilFunc, material.stencilRef, material.stencilFuncMask );
this.setStencilOp( material.stencilFail, material.stencilZFail, material.stencilZPass );
const hasSeparateBack =
material.stencilBackFunc !== null ||
material.stencilBackFail !== null ||
material.stencilBackZFail !== null ||
material.stencilBackZPass !== null ||
material.stencilBackRef !== null;

if ( hasSeparateBack ) {

this.setStencilMaskSeparate( material.stencilWriteMask, material.stencilWriteMask );

this.setStencilFuncSeparate(
material.stencilFunc, material.stencilRef, material.stencilFuncMask,
material.stencilBackFunc !== null ? material.stencilBackFunc : material.stencilFunc,
material.stencilBackRef !== null ? material.stencilBackRef : material.stencilRef,
material.stencilFuncMask
);

this.setStencilOpSeparate(
material.stencilFail, material.stencilZFail, material.stencilZPass,
material.stencilBackFail !== null ? material.stencilBackFail : material.stencilFail,
material.stencilBackZFail !== null ? material.stencilBackZFail : material.stencilZFail,
material.stencilBackZPass !== null ? material.stencilBackZPass : material.stencilZPass
);

} else {

this.setStencilMask( material.stencilWriteMask );
this.setStencilFunc( material.stencilFunc, material.stencilRef, material.stencilFuncMask );
this.setStencilOp( material.stencilFail, material.stencilZFail, material.stencilZPass );

}

}

Expand Down
Loading
Loading