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?
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>
);
}