Building an Interactive Dropdown Menu in Three.js Using Plane Geometry and Canvas Texture

Step-by-Step Implementation

1. Set Up the Scene

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.z = 5;

2. Define the Dropdown Texture

let dropdownOpen = false;
let selectedOption = 'Option 1';
const options = ['Option 1', 'Option 2', 'Option 3'];

function createDropdownTexture() {
  const canvas = document.createElement('canvas');
  canvas.width = 256;
  canvas.height = 256;
  const ctx = canvas.getContext('2d');

  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = '#333';
  ctx.fillRect(20, 20, 216, 40); // closed dropdown or selected option
  ctx.fillStyle = '#fff';
  ctx.font = '20px sans-serif';
  ctx.fillText(selectedOption, 30, 48);

  if (dropdownOpen) {
    for (let i = 0; i < options.length; i++) {
      ctx.fillStyle = '#ccc';
      ctx.fillRect(20, 70 + i * 40, 216, 40);
      ctx.fillStyle = '#000';
      ctx.fillText(options[i], 30, 98 + i * 40);
    }
  }

  return new THREE.CanvasTexture(canvas);
}

3. Create and Add the Plane

let dropdownTexture = createDropdownTexture();
const material = new THREE.MeshBasicMaterial({ map: dropdownTexture, transparent: true });
const geometry = new THREE.PlaneGeometry(2.5, 2.5);
const dropdownPlane = new THREE.Mesh(geometry, material);
scene.add(dropdownPlane);

4. Handle Raycasting and Interaction

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function onClick(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObject(dropdownPlane);

  if (intersects.length > 0) {
    const uv = intersects[0].uv;
    const x = uv.x * 256;
    const y = (1 - uv.y) * 256;

    if (!dropdownOpen && x >= 20 && x <= 236 && y >= 20 && y <= 60) {
      dropdownOpen = true;
    } else if (dropdownOpen && y >= 70 && y <= 190) {
      const index = Math.floor((y - 70) / 40);
      if (options[index]) {
        selectedOption = options[index];
        dropdownOpen = false;
      }
    } else {
      dropdownOpen = false;
    }

    updateDropdown();
  }
}

function updateDropdown() {
  dropdownTexture = createDropdownTexture();
  material.map = dropdownTexture;
  material.map.needsUpdate = true;
}

window.addEventListener('click', onClick);

5. Render Loop

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

✅ Result

You now have a 3D dropdown menu inside a plane that:

  • Opens and closes when clicked.
  • Lets users select an option.
  • Renders options and state dynamically within a CanvasTexture.

All this happens inside the 3D scene, with no HTML/CSS elements on top of your WebGL canvas.

Leave a comment

Your email address will not be published. Required fields are marked *