Stencil buffer - how to make a mesh to be visible through another mesh only?

Ive tried many different options but still can’t understand why the red plane is visible always while i want it to be visible through green planes only.

const renderer = new THREE.WebGLRenderer({
  antialias: false,
  precision: 'highp',
  reverseDepthBuffer: false,
  stencil: true,
});

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = 0xffffff;
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.set(-20, 20.5, 5);

const controls = new PointerLockControls(camera, renderer.domElement);

const floorSize = 100;
const floorGeometry = new THREE.PlaneGeometry(floorSize, floorSize);
const floorMaterial = new THREE.MeshStandardMaterial({
  color: 0xff0000,
  stencilWrite: true,
  stencilFunc: THREE.NotEqualStencilFunc,
  stencilZPass: THREE.KeepStencilOp,
  stencilRef: 1,
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
floor.renderOrder = 5;

const grassParams = {
  count: 20,
  width: 12.05,
  height: { min: 12.0, max: 12.5 },
  areaSize: floorSize - 10,
  density: 0.1,
  scaleVariation: 0.3,
  color: 0x4a6b2a,
};

const grassGeometry = new THREE.PlaneGeometry(grassParams.width, 1);
const grassMaterial = new THREE.MeshStandardMaterial({
  color: grassParams.color,
  side: THREE.DoubleSide,
  transparent: true,
  opacity: 0.5,
  stencilWrite: true,
  stencilFunc: THREE.AlwaysStencilFunc,
  stencilZPass: THREE.ReplaceStencilOp,
  stencilRef: 1,
  depthWrite: false,
});

const grassMesh = new THREE.InstancedMesh(
  grassGeometry,
  grassMaterial,
  grassParams.count
);
grassMesh.castShadow = true;
grassMesh.receiveShadow = true;
scene.add(grassMesh);
grassMesh.renderOrder = 1;

const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();

for (let i = 0; i < grassParams.count; i++) {
  const radius =
    Math.pow(Math.random(), grassParams.density) * (grassParams.areaSize / 2);
  const angle = Math.random() * Math.PI * 2;

  position.x = radius * Math.cos(angle);
  position.z = radius * Math.sin(angle);
  position.y = 0;

  const height = THREE.MathUtils.lerp(
    grassParams.height.min,
    grassParams.height.max,
    Math.random()
  );

  const widthScale = 1 + (Math.random() - 0.5) * grassParams.scaleVariation;
  const heightScale =
    height * (1 + (Math.random() - 0.5) * grassParams.scaleVariation);

  scale.set(widthScale, heightScale, 1);

  quaternion.setFromEuler(
    new THREE.Euler(
      (Math.random() - 0.5) * 0.2,
      Math.random() * Math.PI * 2,
      0
    )
  );

  position.y = height / 2;

  matrix.compose(position, quaternion, scale);
  grassMesh.setMatrixAt(i, matrix);
}

grassMesh.instanceMatrix.needsUpdate = true;

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);

const moveSpeed = 0.075;
const moveState = {
  forward: false,
  backward: false,
  left: false,
  right: false,
  up: false,
  down: false,
};

document.addEventListener('click', () => controls.lock());

const keyMap = {
  KeyW: 'forward',
  KeyA: 'left',
  KeyS: 'backward',
  KeyD: 'right',
  Space: 'up',
  ShiftLeft: 'down',
};

document.addEventListener('keydown', e => (moveState[keyMap[e.code]] = true));
document.addEventListener('keyup', e => (moveState[keyMap[e.code]] = false));

function animate() {
  requestAnimationFrame(animate);

  if (controls.isLocked) {
    if (moveState.forward) controls.moveForward(moveSpeed);
    if (moveState.backward) controls.moveForward(-moveSpeed);
    if (moveState.left) controls.moveRight(-moveSpeed);
    if (moveState.right) controls.moveRight(moveSpeed);
    if (moveState.up) camera.position.y += moveSpeed;
    if (moveState.down) camera.position.y -= moveSpeed;
  }

  renderer.render(scene, camera);
}

If without stencil buffer, then onBeforeRender grass, render floor to texture and put this texture to grass uniform like texture. And modify grass shader to display floor.

This option doesnt fit me because my current example uses planes just for simplicity. I’m going to use it further with more complex and unpredictable shapes. Also i’d like to learn how to use stencil buffer and understand what have i done wrong.

I got the desired result of “window” using stencil test. So a red plane is visible inside a green mesh but only when the green plane has transparent = false. I suppose visual transparency effect should be used via alphaTest only to combine it with stencil test?

const renderer = new THREE.WebGLRenderer({
  antialias: false,
  precision: 'highp',
  reverseDepthBuffer: false,
  stencil: true,
});
renderer.setClearAlpha(0.0);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = 0xffffff;
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.set(0, 10, 120);

const controls = new PointerLockControls(camera, renderer.domElement);

const floorSize = 100;
const floorGeometry = new THREE.PlaneGeometry(floorSize, floorSize);
const floorMaterial = new THREE.MeshStandardMaterial({
  color: 0xff0000,
  side: THREE.DoubleSide,
  stencilWrite: true,
  stencilFunc: THREE.EqualStencilFunc,
  stencilZPass: THREE.KeepStencilOp,
  stencilRef: 1,
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
floor.renderOrder = 5;

const grassParams = {
  count: 20,
  width: 5.0,
  height: { min: 12.0, max: 12.5 },
  areaSize: floorSize - 10,
  density: 0.1,
  scaleVariation: 0.3,
  color: 0x4a6b2a,
};

const grassGeometry = new THREE.PlaneGeometry(grassParams.width, 5);
const grassMaterial = new THREE.MeshStandardMaterial({
  color: grassParams.color,
  side: THREE.DoubleSide,
  // transparent: true,
  // opacity: 0.5,
  stencilWrite: true,
  stencilFunc: THREE.AlwaysStencilFunc,
  stencilZPass: THREE.ReplaceStencilOp,
  stencilRef: 1,
});

const grassMesh = new THREE.InstancedMesh(
  grassGeometry,
  grassMaterial,
  grassParams.count
);
grassMesh.castShadow = true;
grassMesh.receiveShadow = true;
scene.add(grassMesh);
grassMesh.renderOrder = 1;

const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();

for (let i = 0; i < grassParams.count; i++) {
  const radius =
    Math.pow(Math.random(), grassParams.density) * (grassParams.areaSize / 2);
  const angle = Math.random() * Math.PI * 2;

  position.x = radius * Math.cos(angle);
  position.z = radius * Math.sin(angle);
  position.y = 0;

  const height = THREE.MathUtils.lerp(
    grassParams.height.min,
    grassParams.height.max,
    Math.random()
  );

  const widthScale = 1 + (Math.random() - 0.5) * grassParams.scaleVariation;
  const heightScale =
    height * (1 + (Math.random() - 0.5) * grassParams.scaleVariation);

  scale.set(widthScale, heightScale, 1);

  quaternion.setFromEuler(
    new THREE.Euler(
      (Math.random() - 0.5) * 0.2,
      Math.random() * Math.PI * 2,
      0
    )
  );

  position.y = height / 2;

  matrix.compose(position, quaternion, scale);
  grassMesh.setMatrixAt(i, matrix);
}

grassMesh.instanceMatrix.needsUpdate = true;

const ambientLight = new THREE.AmbientLight(0xffffff, 5.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 5.0);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);

const moveSpeed = 0.075;
const moveState = {
  forward: false,
  backward: false,
  left: false,
  right: false,
  up: false,
  down: false,
};

document.addEventListener('click', () => controls.lock());

const keyMap = {
  KeyW: 'forward',
  KeyA: 'left',
  KeyS: 'backward',
  KeyD: 'right',
  Space: 'up',
  ShiftLeft: 'down',
};

document.addEventListener('keydown', e => (moveState[keyMap[e.code]] = true));
document.addEventListener('keyup', e => (moveState[keyMap[e.code]] = false));

function animate() {
  requestAnimationFrame(animate);

  if (controls.isLocked) {
    if (moveState.forward) controls.moveForward(moveSpeed);
    if (moveState.backward) controls.moveForward(-moveSpeed);
    if (moveState.left) controls.moveRight(-moveSpeed);
    if (moveState.right) controls.moveRight(moveSpeed);
    if (moveState.up) camera.position.y += moveSpeed;
    if (moveState.down) camera.position.y -= moveSpeed;
  }

  renderer.render(scene, camera);
}

animate();

I dont know how to use stencil