Search code examples
react-nativethree.jsexpo

Detect when object is touched inside pure three js scene


I am building an expo native app, i am using three.js and expo-gl, without fiber and drei packages. I have a scene with some spheres inside, how i can detect when i touch some object inside scene?

Scene is rendered like

<GLView 
  {...panResponder.panHandlers}
  style={{ flex: 1 }} 
  onContextCreate={onContextCreate} 
  onTouchStart={handleTouch} 
/>



const handleTouch = (event) => {
    const { locationX, locationY } = event.nativeEvent;
    touch.x = (locationX / event.target.clientWidth) * 2 - 1;
    touch.y = -(locationY / event.target.clientHeight) * 2 + 1;

    raycaster.setFromCamera(touch, cameraRef.current);
    const intersects = raycaster.intersectObject(planet);;
    if (intersects.length > 0) {
      console.log(intersects);
    }
  };

With planet defined with

class Planet extends Mesh {
  constructor() {
    super(
      new SphereGeometry(1.0, 32, 32),
      new MeshStandardMaterial({
        map: new TextureLoader().load(require("../../icon.jpg")),
      })
    );
  }
}

It detects when scene is touched, but no insersects are found wherever i touch.

What i am missing?


Solution

  • This sounds like a coordinate problem, because you say the scene responds to touch but doesn't respond in a specific location. I experimented a bit, but this solution has not been tested on a physical device, only on snack.expo. I suspect you should take a closer look at the event.target.clientWidth and event.target.clientHeight properties. To use the correct width and height try using the onLayout handler on the parent <View> to obtain properly normalize touch coordinates [−1,1].

    import React, { useRef, useEffect, useState } from 'react';
    import { View } from 'react-native';
    import { GLView } from 'expo-gl';
    import { Renderer,TextureLoader } from 'expo-three';
    import {
      MeshStandardMaterial,
      AmbientLight,
      Mesh,
      PerspectiveCamera,
      Scene,
      Raycaster,
      SphereGeometry,
      Vector2,
    } from 'three';
    
    export default function App() {
      const [glContext, setGLContext] = useState(null);
      const [layout, setLayout] = useState({ width: 0, height: 0 });
      const cameraRef = useRef();
      const sceneRef = useRef();
      const raycaster = useRef(new Raycaster());
      const spheresRef = useRef([]);
    
      useEffect(() => {
        if (!glContext) return;
    
        const { drawingBufferWidth: width, drawingBufferHeight: height } = glContext;
        const pixelStorei = glContext.pixelStorei.bind(glContext);
        glContext.pixelStorei = function (...args) {
          const [parameter] = args;
          switch (parameter) {
            case glContext.UNPACK_FLIP_Y_WEBGL:
              return pixelStorei(...args);
          }
        };
        const renderer = new Renderer({ gl: glContext });
        renderer.setSize(width, height);
    
        const scene = new Scene();
        sceneRef.current = scene;
    
        const camera = new PerspectiveCamera(75, width / height, 0.1, 1000);
        camera.position.z = 8;
        cameraRef.current = camera;
    
        const ambientLight = new AmbientLight(0xffffff);
        scene.add(ambientLight);
    
        const spheres = [];
        const columns = 2;
        const rows = 5;
        const distanceX = 2;
        const distanceY = 2;
        const offsetX = (columns - 1) / 2;
        const offsetY = (rows - 1) / 2;
    
        for (let i = 0; i < rows; i++) {
          for (let j = 0; j < columns; j++) {
            const sphere = createSphere();
            sphere.position.x = (j - offsetX) * distanceX;
            sphere.position.y = (offsetY - i) * distanceY;
            scene.add(sphere);
            spheres.push(sphere);
          }
        }
        spheresRef.current = spheres;
    
        const render = () => {
          requestAnimationFrame(render);
          renderer.render(scene, camera);
          glContext.endFrameEXP();
        };
        render();
      }, [glContext]);
    
      const createSphere = () => {
        const geometry = new SphereGeometry(0.8, 32, 32);
        const texture = new TextureLoader().load(
          require('./assets/snack-icon.png')
        );
        const material = new MeshStandardMaterial({
          map: texture,
        });
        return new Mesh(geometry, material);
      };
    
      const handleTouch = (event) => {
        const { locationX, locationY } = event.nativeEvent;
        const { width, height } = layout;
    
        const touch = new Vector2(
          (locationX / width) * 2 - 1,
          -(locationY / height) * 2 + 1
        );
    
        raycaster.current.setFromCamera(touch, cameraRef.current);
    
        const intersects = raycaster.current.intersectObjects(spheresRef.current, true);
        if (intersects.length > 0) {
          const touchedSphere = intersects[0].object;
          touchedSphere.material.map = null;
          touchedSphere.material.color.set(0xff0000);
          touchedSphere.material.needsUpdate = true;
          console.log('Touched sphere:', touchedSphere);
        }
      };
    
      return (
        <View
          style={{ flex: 1 }}
          onLayout={(event) => {
            const { width, height } = event.nativeEvent.layout;
            setLayout({ width, height });
          }}
        >
          <GLView
            style={{ flex: 1 }}
            onContextCreate={setGLContext}
            onTouchStart={handleTouch}
          />
        </View>
      );
    }