I am trying to build a minecraft clone in threejs currently.
I have a very basic setup right now where I have added some cubes to a scene and a Raycaster, that checks if the Pointer is over any of the cubes in the scene and if so creating an indicator cube for removing/adding a cube.
When in Removal mode, the cube the pointer is over shall be highlighted in red and when clicked, remove that cube. Immediately after that, the Raycaster is called again, to check if the Pointer is now hovering over any cubes that were behind the one that has been removed and then indicate in red the next cube so on and so forth.
That is working great so far.
Now I have added the option to add cubes on click. When the pointer is hovering over a cube, the indicator shall now be placed next to the face of the cube where the pointer is hovering over, to indicate where the new cube would be placed. This is also working.
Here comes the point that is not working:
When clicking to add the new cube, the Raycaster is called again. But differently to when I removed the cube, the Raycaster is not recognizing the new intersection with the newly created cube, thus not updating the position of the indicator immediately.
Only after moving the mouse by at least one pixel, the raycaster is recognizing the new cube and updating the indicator.
I have created a minimal Threejs file. Pressing 1 brings you into Add Mode and pressing 2 into Removal Mode.
import * as THREE from 'three'
// Setup scene
const scene = new THREE.Scene()
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000)
const cameraDistance = 6
camera.position.set(cameraDistance, 4, cameraDistance)
camera.lookAt(new THREE.Vector3(0, 0, 0))
// Add example cubes
const cube = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 'gray', wireframe: false })
const mesh1 = new THREE.Mesh(cube, material)
const mesh2 = new THREE.Mesh(cube, material)
mesh2.position.set(1, 0, 0)
const mesh3 = new THREE.Mesh(cube, material)
mesh3.position.set(0, 1, 0)
const cubes = [mesh1, mesh2, mesh3]
scene.add(mesh1)
scene.add(mesh2)
scene.add(mesh3)
// Create indicator mesh that shows where the next cube will be placed/removed
const indicatorGeo = new THREE.BoxGeometry(1.01, 1.01, 1.01)
const indicatorMaterial = new THREE.MeshBasicMaterial({
color: 'red',
opacity: 0.2,
transparent: true,
wireframe: false,
})
const indicatorMesh = new THREE.Mesh(indicatorGeo, indicatorMaterial)
indicatorMesh.name = 'indicatorMesh'
indicatorMesh.position.set(0.5, 0.5, 0.5)
indicatorMesh.visible = false
scene.add(indicatorMesh)
let raycaster = new THREE.Raycaster()
let pointer = new THREE.Vector2()
// Mode can be changed by pressing 1 or 2
let MODE = 'remove'
// This is called on every pointermove event, and explicitly when a cube is added or removed
function checkIntersection() {
pointer.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1)
raycaster.setFromCamera(pointer, camera)
let intersects = raycaster.intersectObjects(cubes, true)
if (intersects.length > 0) {
const intersect = intersects[0]
indicatorMesh.position.copy(intersect.object.position)
if (MODE === 'add') {
indicatorMesh.position.add(intersect.face.normal)
}
indicatorMesh.visible = true
} else {
indicatorMesh.visible = false
}
}
document.addEventListener('pointermove', checkIntersection)
document.addEventListener('pointerdown', (event) => {
pointer.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1)
raycaster.setFromCamera(pointer, camera)
let intersects = raycaster.intersectObjects(cubes, true)
if (intersects.length > 0) {
const intersect = intersects[0]
if (MODE === 'remove') {
cubes.splice(cubes.indexOf(intersect.object), 1)
scene.remove(intersect.object)
} else if (MODE === 'add') {
const voxel = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 'gray', wireframe: false }),
)
voxel.position.copy(intersect.object.position).add(intersect.face.normal)
cubes.push(voxel)
scene.add(voxel)
}
checkIntersection()
}
})
document.addEventListener('keydown', (event) => {
switch (event.key) {
case '1':
MODE = 'add'
checkIntersection(event)
break
case '2':
MODE = 'remove'
checkIntersection(event)
break
}
})
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
I will also add videos of what is happening when removing and adding. Note how the red indicator is immediately updated when clicking (You can try this out yourself) in Removal Mode, but only updating once the pointer is moved in Add Mode
I tried calling the checkIntersection function on the pointerup event, as a workaround, which does work, but it not what I am intending to do.
I also tried logging the intersections array, which confirms my suspicion, that the new object is not being recognized by the raycaster. I also tried adding the new cube manually to the intersections object but that didn’t help.
I am doubly confused why everything works as expected when in Removal Mode but I assume it could be, because the object is immediately removed from the scene?
What am I missing. I assume this must be possible but I am missing something here.