Make camera fly to object and fit to fiew - three.js

I am bit struggle to solve this fit to view object. I want camera fly to a object and centering the screen. Currently just got camera to the object position.
private fitToView = (objSelected: Object3D | undefined) => {
if (objSelected) {
const coord = {
x: this.camera.position.x,
y: this.camera.position.y,
z: this.camera.position.z,
};
this.orbitControl.enabled = false;
const worldPosition = new Vector3();
objSelected.getWorldPosition(worldPosition);
const newCoord = {
x: worldPosition.x,
y: worldPosition.y + 2,
z: worldPosition.z,
};
const tweenCamera = new Tween(coord)
.to(newCoord, 1500)
.easing(Easing.Quadratic.InOut)
.onUpdate(() => {
this.camera.position.set(coord.x, coord.y, coord.z);
})
.onComplete(() => {
this.orbitControl.enabled = true;
this.orbitControl.update();
});
tweenCamera.start();
}
};
This picture I expected fit view after clicking object.

Related

Cannon.js and Three.js – Character Control with Physics

I am trying to build a little world with a Third Person Controller.
For the character, I followed this tutorial by SimonDev and just changed the model from a FBX to GLTF.
Now, I try to implement a Physics World with Cannon.js.
I got it to the point where the collider body is positioned at the starting point of the model. But it stays there after I move the model. I need the collider body to be attached at the model.
I know that I should move the collider body and update the character model to that position but I cannot get it to work. This is my current code. Maybe it is just a simple solution but I am new to Cannon.js, so any help is appreciated. Thank you!
class BasicCharacterController {
constructor(experience, params) {
this.experience = experience;
this._Init(params);
}
_Init(params) {
this._params = params;
this._decceleration = new THREE.Vector3(-0.0005, -0.0001, -5.0);
this._acceleration = new THREE.Vector3(1, 0.25, 50.0);
this._velocity = new THREE.Vector3(0, 0, 0);
this._position = new THREE.Vector3();
this._animations = {};
this._input = new BasicCharacterControllerInput();
this._stateMachine = new CharacterFSM(
new BasicCharacterControllerProxy(this._animations));
this._LoadModels();
}
_LoadModels() {
this.physicsCharacterShape = new CANNON.Box(new CANNON.Vec3(0.5, 1, 0.5));
this.physicsCharacterBody = new CANNON.Body({
mass: 0,
shape: this.physicsCharacterShape,
position: new CANNON.Vec3(0, 0, 0)
});
this.experience.physicsWorld.addBody(this.physicsCharacterBody);
this.gltfLoader = new GLTFLoader();
this.gltfLoader.setPath('./sources/assets/');
this.gltfLoader.load('VSLBINA_TPOSE_GLTF.gltf', (gltf) => {
gltf.scene.traverse(c => {
c.castShadow = true;
});
this._target = gltf.scene;
this._params.scene.add(this._target);
this._target.position.copy(this.physicsCharacterBody.position);
this._target.quaternion.copy(this.physicsCharacterBody.quaternion);
this._mixer = new THREE.AnimationMixer(this._target);
this._manager = new THREE.LoadingManager();
this._manager.onLoad = () => {
this._stateMachine.SetState('idle');
};
const _OnLoad = (animName, anim) => {
const clip = anim.animations[0];
const action = this._mixer.clipAction(clip);
this._animations[animName] = {
clip: clip,
action: action,
};
};
const loader = new GLTFLoader(this._manager);
loader.setPath('./sources/assets/');
loader.load('VSLBINA_WALKING_GLTF.gltf', (a) => { _OnLoad('walk', a); });
loader.load('VSLBINA_IDLE_GLTF.gltf', (a) => { _OnLoad('idle', a); });
});
}
get Position() {
return this._position;
}
get Rotation() {
if (!this._target) {
return new THREE.Quaternion();
}
return this._target.quaternion;
}
Update(timeInSeconds) {
if (!this._stateMachine._currentState) {
return;
}
this._stateMachine.Update(timeInSeconds, this._input);
const velocity = this._velocity;
const frameDecceleration = new THREE.Vector3(
velocity.x * this._decceleration.x,
velocity.y * this._decceleration.y,
velocity.z * this._decceleration.z
);
frameDecceleration.multiplyScalar(timeInSeconds);
frameDecceleration.z = Math.sign(frameDecceleration.z) * Math.min(
Math.abs(frameDecceleration.z), Math.abs(velocity.z));
velocity.add(frameDecceleration);
const controlObject = this._target;
const _Q = new THREE.Quaternion();
const _A = new THREE.Vector3();
const _R = controlObject.quaternion.clone();
const acc = this._acceleration.clone();
if (this._input._keys.shift) {
acc.multiplyScalar(2.0);
}
if (this._input._keys.forward) {
velocity.z += acc.z * timeInSeconds;
}
if (this._input._keys.backward) {
velocity.z -= acc.z * timeInSeconds;
}
if (this._input._keys.left) {
_A.set(0, 1, 0);
_Q.setFromAxisAngle(_A, 4.0 * Math.PI * timeInSeconds * this._acceleration.y);
_R.multiply(_Q);
}
if (this._input._keys.right) {
_A.set(0, 1, 0);
_Q.setFromAxisAngle(_A, 4.0 * -Math.PI * timeInSeconds * this._acceleration.y);
_R.multiply(_Q);
}
controlObject.quaternion.copy(_R);
const oldPosition = new THREE.Vector3();
oldPosition.copy(controlObject.position);
const forward = new THREE.Vector3(0, 0, 1);
forward.applyQuaternion(controlObject.quaternion);
forward.normalize();
const sideways = new THREE.Vector3(1, 0, 0);
sideways.applyQuaternion(controlObject.quaternion);
sideways.normalize();
sideways.multiplyScalar(velocity.x * timeInSeconds);
forward.multiplyScalar(velocity.z * timeInSeconds);
controlObject.position.add(forward);
controlObject.position.add(sideways);
this._position.copy(controlObject.position);
if (this._mixer) {
this._mixer.update(timeInSeconds);
};
// Physics Collider Body
// if (this._target) {
// this._target.position.copy(this.physicsCharacterBody.position);
// this._target.quaternion.copy(this.physicsCharacterBody.quaternion);
// }
}
};

How to use Threejs to create a boundary for the Mesh load by fbxloador

The created boundary's scale and rotation are totally different with the import fbxmodel.
Hi, I have loaded a fbx model into the scene by fbxLoador.
const addFbxModel = (modelName: string, position: Vector3) => {
const fbxLoader = new FBXLoader();
fbxLoader.load(
`../../src/assets/models/${modelName}.fbx`,
(fbxObject: Object3D) => {
fbxObject.position.set(position.x, position.y, position.z);
console.log(fbxObject.scale);
fbxObject.scale.set(0.01, 0.01, 0.01);
const material = new MeshBasicMaterial({ color: 0x008080 });
fbxObject.traverse((object) => {
if (object instanceof Mesh) {
object.material = material;
}
});
scene.add(fbxObject);
updateRenderer();
updateCamera();
render();
},
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
},
(error) => {
console.log(error);
}
);
};
And now i want to add a click function on this model which will highlight it by showing it's boundary box.
const onclick = (event: MouseEvent) => {
event.preventDefault();
if (renderBox.value) {
const mouse = new Vector2();
mouse.x = (event.offsetX / renderBox.value.clientWidth) * 2 - 1;
mouse.y = -(event.offsetY / renderBox.value.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const intersect = intersects[0];
const object = intersect.object;
createBoundary(object);
}
}
};
const createBoundary = (object: Object3D) => {
if (object instanceof Mesh) {
console.log(object.geometry, object.scale, object.rotation);
const edges = new EdgesGeometry(object.geometry);
const material = new LineBasicMaterial({ color: 0xffffff });
const wireframe = new LineSegments(edges, material);
wireframe.scale.copy(object.scale);
wireframe.rotation.copy(object.rotation);
console.log(wireframe.scale);
scene.add(wireframe);
}
};
But now the boundary's scale and rotation are totally different with the fbxmodel.
And also the boundary is too complex, is it possible to create one only include the outline and the top point.
Thank you. :)

Drag&Drop element into canvas from HTML

I've got a very trivial task to drag&drop the element from a gallery into the Three.js canvas.
Nothing seems tricky until I face the problem that when I drag the item and add it to the scene I cannot update the item's coordinates until I finish the drag&drop event
I already played around with all events that watch the mouse event mousemove, drag, dragover but the element is just stuck at the initial coordinates, the ones I applied in the dragenter event
export const params = {
devicePixelRatio: Math.min(window.devicePixelRatio, 2),
size: getSizeParams(),
grid: {
size: 20,
divisions: 20,
},
}
const itemProtos = ['Box', 'Sphere', 'Cone']
export const canvas = document.querySelector(`#canvas`)
const raycaster = new Raycaster()
const pointer = new Vector2()
const scene = new Scene()
const camera = new PerspectiveCamera(75, params.size.width / params.size.height, 0.1, 100)
camera.position.z = 5
camera.position.y = 2
/**
* Variable for Drag&Drop - just created object that's being moved around
*/
let newObjectType = null
let newObject = null
/**
* Groups
*/
export const itemGroup = new Group()
scene.add(itemGroup)
/**
* Grid
*/
export const gridHelper = new GridHelper(params.grid.size, params.grid.divisions)
scene.add(gridHelper)
/**
* Renderer
*/
const renderer = new WebGLRenderer({
canvas,
antialias: true,
})
renderer.setSize(params.size.width, params.size.height)
renderer.setPixelRatio(params.devicePixelRatio)
/**
* Resizing updates to fit the screen
*/
window.addEventListener('resize', () => {
params.size = getSizeParams()
camera.aspect = params.size.width / params.size.height
camera.updateProjectionMatrix()
renderer.setSize(params.size.width, params.size.height)
renderer.setPixelRatio()
})
canvas.addEventListener('mouseenter', () => {
canvas.style.cursor = 'grab'
})
/**
* Controls
*/
const orbitControls = new OrbitControls(camera, canvas)
orbitControls.enableDamping = true
orbitControls.addEventListener('start', () => {
canvas.style.cursor = 'grabbing'
})
orbitControls.addEventListener('end', () => {
canvas.style.cursor = 'grab'
})
const tick = () => {
orbitControls.update()
requestAnimationFrame(tick)
renderer.render(scene, camera)
}
window.onload = tick
/**
* Raycaster functions
*/
const refreshMouseCoords = (event) => {
pointer.x = (event.clientX / params.size.width) * 2 - 1
pointer.y = -(event.clientY / params.size.height) * 2 + 1
}
let currentIntersect = null
let currentPick = null
canvas.addEventListener('mousemove', (event) => {
refreshMouseCoords(event)
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObjects(itemGroup.children, false)
if(intersects.length && intersects[0].object instanceof Mesh) {
if(!currentIntersect) {
canvas.style.cursor = 'all-scroll'
intersects[0].object.material.color.set('red')
console.log(`mouse enter`)
}
if(currentIntersect && currentIntersect !== intersects[0].object) {
currentIntersect.material.color.set('blue')
intersects[0].object.material.color.set('red')
}
currentIntersect = intersects[0].object
} else {
if(currentIntersect) {
console.log(`mouse leave`)
currentIntersect.material.color.set('blue')
canvas.style.cursor = 'grab'
}
currentIntersect = null
}
moveItem(currentPick)
})
/**
* Function to move items around GridHelper
*/
const moveItem = (item) => {
const intersectsGround = raycaster.intersectObject(gridHelper, false)
if(item && intersectsGround[0]) {
item.position.z = intersectsGround[0].point.z
item.position.x = intersectsGround[0].point.x
}
}
canvas.addEventListener('mousedown', (event) => {
event.preventDefault()
refreshMouseCoords(event)
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObjects(itemGroup.children, false)
if(intersects.length && intersects[0].object instanceof Mesh) {
currentPick = intersects[0].object
}
if(currentIntersect) {
canvas.style.cursor = 'all-scroll'
orbitControls.enabled = false
}
})
canvas.addEventListener('mouseup', () => {
if(currentIntersect) {
canvas.style.cursor = 'all-scroll'
orbitControls.enabled = true
}
if(currentPick) {
currentPick = null
}
})
// *** Drag&Drop *** //
const gallery = document.querySelector(`#gallery`)
setGallery(itemProtos, gallery)
canvas.addEventListener('dragenter', (event) => {
event.preventDefault()
console.log('Drag&Drop: dragenter')
refreshMouseCoords(event)
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObject(gridHelper)
if (intersects.length && newObjectType) {
add3DEl(intersects[0].point, newObjectType, itemGroup)
}
})
canvas.addEventListener('dragover', (event) => {
event.preventDefault()
event.stopPropagation()
if(newObject) {
moveItem(newObject)
}
})
function setGallery(itemProtos, gallery) {
for (let i of itemProtos) {
const el = createProto()
gallery.appendChild(el)
el.addEventListener('dragstart', function (event) {
event.dataTransfer.setData('text/plain', i)
newObjectType = i
})
el.addEventListener('dragend', function () {
newObjectType = null
newObject = null
})
}
}
function add3DEl({ x, z }, type = 'Box', scene) {
const geometry = new itemObjects[`${type}Geometry`]()
const material = new MeshBasicMaterial({ color: 0x0000ff }) // Shared material for all items
material.wireframe = true
const el = new Mesh(geometry, material)
el.position.x = x
el.position.y = type == 'Sphere' ? 1 : .5
el.position.z = z
el.userData.name = `${type}_${Date.now()}`
newObject = el
scene.add(el)
}
Here you can find a playgroud with what I've got so far: Playground

How to control movement of a person in react three fiber?

I want to create a game where I have to make my model controllable with keyboard input. I don't know what's the best way to do it and how to implement it properly.
We can implement this with cannon.js
Create a custom hook to listen to user's input.
const usePersonControls = () => {
const keys = {
KeyW: 'forward',
KeyS: 'backward',
KeyA: 'left',
KeyD: 'right',
Space: 'jump',
}
const moveFieldByKey = (key) => keys[key]
const [movement, setMovement] = useState({
forward: false,
backward: false,
left: false,
right: false,
jump: false,
})
useEffect(() => {
const handleKeyDown = (e) => {
setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: true }))
}
const handleKeyUp = (e) => {
setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: false }))
}
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [])
return movement
}
Now use it like so,
const { forward, backward, left, right, jump } = usePersonControls()
Create a body for person in Cannon World.
const [mesh, api] = useSphere(() => ({
mass: 10,
position: [0, 1, 0],
type: 'Dynamic',
}))
Apply velocity to sphere body.
useFrame(() => {
// Calculating front/side movement ...
let frontVector = new Vector3(0,0,0);
let sideVector = new Vector3(0,0,0);
let direction = new Vector3(0,0,0);
frontVector.set(0, 0, Number(forward) - Number(backward))
sideVector.set(Number(right) - Number(left), 0, 0)
direction
.subVectors(frontVector, sideVector)
.normalize()
.multiplyScalar(SPEED)
api.velocity.set(direction.x, 0, direction.z)
})
Now our sphere body is able to move with user's input in Cannon World, so now just update your player model in fiber using sphere's position on each frame, like so
// Setting person model position to sphere body position ...
useFrame(() => {
...
mesh.current.getWorldPosition(playerModelReference.current.position)
})
Sorry for long explanation, hope you find it helpful.
look here for details on the where does frontVector gets defined
Line 24:9: 'frontVector' is not defined
How to make character to move around 3D world in React Three Fiber?
import React from "react";
import { useSphere } from "#react-three/cannon";
import { useThree, useFrame } from "#react-three/fiber";
import { useKeyboardControls } from "../hooks/useKeyboardControls";
import { Vector3 } from "three";
const SPEED = 6;
export function Person(props) {
const { camera } = useThree();
const { moveForward, moveBackward, moveLeft, moveRight } =
useKeyboardControls();
const [ref, api] = useSphere(() => ({
mass: 1,
type: "Dynamic",
...props,
}));
const velocity = React.useRef([0, 0, 0]);
React.useEffect(() => {
api.velocity.subscribe((v) => (velocity.current = v));
}, [api.velocity]);
useFrame(() => {
camera.position.copy(ref.current.position);
const direction = new Vector3();
const frontVector = new Vector3(
0,
0,
Number(moveBackward) - Number(moveForward)
);
const sideVector = new Vector3(
Number(moveLeft) - Number(moveRight),
0,
0
);
direction
.subVectors(frontVector, sideVector)
.normalize()
.multiplyScalar(SPEED)
.applyEuler(camera.rotation);
api.velocity.set(direction.x, velocity.current[1], direction.z);
});
return (
<>
<mesh ref={ref} />
</>
);
}

Updating three.js texture throughthe dat.gui dropdown

I have loaded different textures using textureLoader and I am trying to update them using dat.gui controls.
Why is the code below not working?
gui.add(mesh.position, "y", -1, 1, 0.1);
gui.add(mesh.material, "map", { alpha: alphaTexture, color: colorTexture, normal: normalTexture })
.onChange(() => {
mesh.material.needsUpdate = true;
console.log("updated");
});
It gives this error:
"Uncaught TypeError: m is undefined" [error][1]
After some tweaking, I found that the values of object(or array) in the third argument only supports string types, so passing a object as a value would not work.
This is the closest workaround that I could think of..
/* GUI options */
const guiOptions = {
mesh_material_map: "color",
};
/* Textures */
const textureLoader = new THREE.TextureLoader(loadingManager);
const colorTexture = textureLoader.load("/textures/door/color.jpg");
const alphaTexture = textureLoader.load("/textures/door/alpha.jpg");
const normalTexture = textureLoader.load("/textures/door/normal.jpg");
const guiTextureHash = {
color: colorTexture,
alpha: alphaTexture,
normal: normalTexture,
};
/* Add to gui */
gui.add(guiOptions, "mesh_material_map", Object.keys(guiTextureHash)).onChange((value) => {
mesh.material.map = guiTextureHash[value];
mesh.needsUpdate = true;
console.log("updated", value);
});
I found your topic looking for a texture picker. It's probably a little away from your starting point but could help some other. I finally made a simple texture picker with a dropdown selection key with dat.gui. The goal is to be able to change on the fly my matcap texture, going through an array of loaded texture.
const gui = new dat.GUI()
const textureLoader = new THREE.TextureLoader()
const myMatCap = [
textureLoader.load('./textures/matcaps/1.png'),
textureLoader.load('./textures/matcaps/2.png'),
textureLoader.load('./textures/matcaps/3.png')
]
const parameters = {
color: 0xff0000,
matCapTexture: 0
}
const updateAllMaterials = () => {
scene.traverse( (child)=>{
if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshMatcapMaterial) {
child.material.matcap = myMatCap[ parameters.matCapTexture]
child.material.needsUpdate = true
}
})
}
gui.add(parameters, 'matCapTexture', {
terracotta: 0,
grey: 1,
chrome: 2,
}).onFinishChange(()=>{
updateAllMaterials()
})
let mesh = new THREE.Mesh(
geometry,
new THREE.MeshMatcapMaterial({
side: THREE.DoubleSide,
matcap: myMatCap[ parameters.matCapTexture ]
})
);
scene.add(mesh)

Resources