First person perspective in threejs - javascript

I've created this example from a GLTF file of Dragonball animation on scroll, but i couldn't achieve my goal which is the user should feel like he is the one riding the car ( 1st person perspective ), How can i do that?
Also i have a question, when the model first appear, it shows the world as a sphere the the camera change its focus to the car, How can focus the camera on 1st person perspective from the beginning?
Here is the example i'm working on:
https://codesandbox.io/s/dbz-snake-path-ecz8ul
import * as THREE from "three";
import { Suspense, useEffect } from "react";
import { Canvas, useFrame, useLoader } from "#react-three/fiber";
import {
ScrollControls,
useScroll,
useAnimations,
Stage
} from "#react-three/drei";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const Model = ({ ...props }) => {
// This hook gives you offets, ranges and other useful things
const scroll = useScroll();
const { scene, animations } = useLoader(GLTFLoader, "/model/scene.gltf");
const { actions } = useAnimations(animations, scene);
let car = scene.getObjectByName("car");
useEffect(() => {
actions["Take 001"].play().paused = true;
}, [actions]);
useFrame((state, delta) => {
// Animate on scroll
const action = actions["Take 001"];
const offset = scroll.offset;
action.time = THREE.MathUtils.damp(
action.time,
action.getClip().duration * offset,
100,
delta
);
// Camera to follow car element
let position = new THREE.Vector3(0, 0, 0);
position.setFromMatrixPosition(car.matrixWorld);
let wDir = new THREE.Vector3(0, 0, 1);
wDir.normalize();
let cameraPosition = position
.clone()
.add(wDir.clone().multiplyScalar(1).add(new THREE.Vector3(0, 0.3, 0)));
wDir.add(new THREE.Vector3(0, 0.2, 0));
state.camera.position.copy(cameraPosition);
state.camera.lookAt(position);
});
return <primitive object={scene} {...props} />;
};
const App = () => {
return (
<>
<Suspense fallback={null}>
<Canvas>
<Stage environment={null}>
<ScrollControls pages={10}>
<Model scale={1} />
</ScrollControls>
</Stage>
</Canvas>
</Suspense>
</>
);
};
export default App;

You should simply add the camera as a child Object of the car. That way when you move the car forward, the camera will immediately follow.
// Add camera as child of car
car.add(camera);
// Now the camera will move and rotate with the car
car.position.set(x, y, z);
car.rotation.y = angle;
You might need to adjust the camera's position at first so it's placed behind the character, or maybe above, that's up to you.

Related

How do I centre a gltf model of the viewport in threejs?

So I'm new to threejs and I've been having an issue with both moving an imported gltf model and moving the camera around the model. My goal is to have the model fit within the canvas dead in the centre. But currently the model seems to be cut off at the top.
if I set camera.up.set(0,0,0). I get the result I want but with the trade-off that the orbit controls don't behave how I wanted them to.
I've tried changing the camera.position.y but that seems to rotate around a point and eventually just gives me a top down view of my model.
This is how it currently looks with the below code:
This is how I want it to look but without having to set camera.up.set(0, 0, 0)
import React, { useEffect } from 'react';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {useSelector} from 'react-redux'
import * as THREE from 'three';
const GarlicSan = () => {
useEffect(() => {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
266 / 400,
45,
4000
);
camera.position.set(
0, 300, 1000
)
//camera.up.set( 0, 0, 0 );
const canvas = document.getElementById('garlic');
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
const updateCanvas = () => {
renderer.setSize( 266, 400);
document.body.appendChild(renderer.domElement);
};
updateCanvas();
// controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = true;
//model loader
const loader = new GLTFLoader();
loader.load('/chibi_garlic/scene.gltf', (gltf) => {
scene.add(gltf.scene)
renderer.render(scene, camera)
});
renderer.setClearColor( 0xffffff, 0);
const animate = () => {
controls.update();
renderer.render(scene, camera);
window.requestAnimationFrame(animate);
};
animate();
}, []);
const type = useSelector((state) => state.content)
return(
<>
{ type.content === 'lobby' ? (
<canvas className=' absolute z-50 left-0 right-0 mx-auto top-[20%]' id='garlic'></canvas>
) : ( <canvas className=' absolute z-50 left-1 transition-all' id='garlic'></canvas>
)}
</>
)
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
For anyone who has this issue. When using orbit controls the target is set to 0, 0, 0. so when changing the camera position the camera will move its x,y,z around that target. My model was placed on top of the 0, 0, 0 mark so I set the controls.target.set(0, 300, 0) once I had defined controls.

Three.js basics – Can't find variable – GLTFLoader animation

I’ve got very basic knowledge of JS. Following three.js docs Loading 3D models I have succesfully rendered 3D object and centered it:
const loader = new GLTFLoader();
loader.load( 'Duck.gltf', function ( duck ) {
const model = duck.scene
const box = new THREE.Box3().setFromObject( model );
const center = new THREE.Vector3();
box.getCenter( center );
model.position.sub( center ); // center the model
scene.add( model );
}, undefined, function ( error ) {
console.error( error );
} );
I would like to animate it now, begining with simple rotation:
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update objects
model.rotation.y = .5 * elapsedTime
// Update Orbital Controls
// controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()
But the problem is console returns:
ReferenceError: Can't find variable: model
You have declared model variable inside the functional scope, try to declare it outside
const loader = new GLTFLoader();
let model, box, center;
loader.load( 'Duck.gltf', function ( duck ) {
model = duck.scene
box = new THREE.Box3().setFromObject( model );
center = new THREE.Vector3();
box.getCenter( center );
model.position.sub( center ); // center the model
scene.add( model );
}, undefined, function ( error ) {
console.error( error );
} );
Hopefully, this will work!
Edited
as #prisoner849 suggested in the comments
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update objects
if (model) {
model.rotation.y = .5 * elapsedTime
}
// Update Orbital Controls
// controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()

Centering and Resizing GLTF models automatically in Three.js

After downloading 3D models from sketchfab.com and importing them into a Three.js scene, they are most of the times not centered and their size is very big.
Is there a way to automatically center and scale gltf models using a script or a software (working on Linux) or otherwise to do it on-the-fly in Three.js and having the camera to move around the object using OrbitControls.
Yep. This code takes a node and finds its size and centerpoint, then rescales it so the max extent is 1 and its centered at 0,0,0, and above the ground on the y axis.
var mroot = yourScene;
var bbox = new THREE.Box3().setFromObject(mroot);
var cent = bbox.getCenter(new THREE.Vector3());
var size = bbox.getSize(new THREE.Vector3());
//Rescale the object to normalized space
var maxAxis = Math.max(size.x, size.y, size.z);
mroot.scale.multiplyScalar(1.0 / maxAxis);
bbox.setFromObject(mroot);
bbox.getCenter(cent);
bbox.getSize(size);
//Reposition to 0,halfY,0
mroot.position.copy(cent).multiplyScalar(-1);
mroot.position.y-= (size.y * 0.5);
var loader = new THREE.GLTFLoader();
loader.load( 'models/yourmodel.gltf',
function ( gltf ) {
gltf.scene.traverse( function ( child ) {
if ( child.isMesh ) {
child.geometry.center(); // center here
}
});
gltf.scene.scale.set(10,10,10) // scale here
scene.add( gltf.scene );
}, (xhr) => xhr, ( err ) => console.error( e ));
To move around the object use controls.target() https://threejs.org/docs/#examples/controls/OrbitControls.target
I resized in REACT like this!
import React, { useState, useRef, useEffect } from "react";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const Earth = () => {
const [model, setModel] = useState();
useEffect(() => {
new GLTFLoader().load("scene.gltf", (model) => {
model.scene.scale.set(0.03, 0.03, 0.03);
setModel(model);
});
});
return model ? <primitive object={model.scene} /> : null;
};
Just call <Earth />
Other proposed solutions did not work well for me.
However, I tested the simple approach below on several models to be loaded into my scenes and it works nicely.
const loader = new GLTFLoader();
loader.load("assets/model_path/scene.gltf",
(gltf) => {
gltf.scene.scale.multiplyScalar(1 / 100); // adjust scalar factor to match your scene scale
gltf.scene.position.x = 20; // once rescaled, position the model where needed
gltf.scene.position.z = -20;
scene.add(gltf.scene);
render();
},
(err) => {
console.error(err);
}
);

How can you make WebGL text geometry selectable after you add it to the scene of the viewer?

After adding a text geometry object to the viewer, I noticed I am not able to select it with the mouse like I can with the rest of the objects in the viewer. How can I make this selectable? I haven't tried anything, as I have not seen any ideas in the docs. I want to be able to listen for the selection event, which I have down, I just don't know how to make this new TextGeometry object selectable. Here is my code, sorry I didn't include it before.
createText (params) {
const geometry = new TextGeometry(params.text,
Object.assign({}, {
font: new Font(FontJson),
params
}))
const material = this.createColorMaterial(
params.color)
const text = new THREE.Mesh(
geometry , material)
text.position.set(
params.position.x,
params.position.y,
params.position.z)
this.viewer.impl.scene.add(text)
this.viewer.impl.sceneUpdated(true)
}
Thank you!
According to my experience, Forge Viewer will only interact with translated models (SVF, F2D) from Forge Model Derivative service, as well as the Built-in APIs. The text geometry seems like a custom one which is created via THREE.TextGeometry, isn't it? You didn't tell much about that.
If you want to interact with custom geometries in Forge Viewer, you must have to implement a Viewer Tool and add some self-logic in, such as highlight a text geometry while mouse clicking. You can refer here for the more details: https://forge.autodesk.com/cloud_and_mobile/2015/03/creating-tools-for-the-view-data-api.html
Since I didn't see any codes from you here, I assume your text is put into _viewer.sceneAfter and here is an example for mouse left clicking:
// change color of custom geometries while mouse clicking
handleSingleClick( event, button ) {
const _viewer = this.viewer;
const intersectObjects = (function () {
const pointerVector = new THREE.Vector3();
const pointerDir = new THREE.Vector3();
const ray = new THREE.Raycaster();
const camera = _viewer.impl.camera;
return function(pointer, objects, recursive) {
const rect = _viewer.impl.canvas.getBoundingClientRect();
const x = (( pointer.clientX - rect.left) / rect.width ) * 2 - 1;
const y = - (( pointer.clientY - rect.top) / rect.height ) * 2 + 1;
if (camera.isPerspective) {
pointerVector.set( x, y, 0.5 );
pointerVector.unproject( camera );
ray.set( camera.position, pointerVector.sub( camera.position ).normalize() );
} else {
pointerVector.set( x, y, -1 );
pointerVector.unproject( camera );
pointerDir.set( 0, 0, -1 );
ray.set( pointerVector, pointerDir.transformDirection( camera.matrixWorld ) );
}
const intersections = ray.intersectObjects( objects, recursive );
return intersections[0] ? intersections[0] : null;
};
})();
const pointer = event.pointers ? event.pointers[ 0 ] : event;
// Get interseted custom geometries
const result = intersectObjects( pointer, _viewer.sceneAfter.children );
if( result && result.object ) {
const mesh = result.object;
// Change object color
let curColor = mesh.material.color;
curColor = ( curColor.getHex() == 0xff0000 ? 0x00ff00 : 0xff0000 );
mesh.material.color.setHex( curColor );
// Rerender
this.viewer.impl.invalidate( true, true, false );
}
return false;
}
Hope it helps.
I wrote an article dedicated to that topic: Handling custom meshes selection along model components in the Forge Viewer

Line disappear when camera move in three js

I'm trying to render d3 force graph in three.js, I'm using standard Line and BoxGeometry with photo texture on it. On force graph update I call draw function, which is also called on in
controls.addEventListener('change', () => {this.redraw()});
but when I move camera, some lines disappear, when I'm closer, it seems worst, besides it does not look like there is any rule, even when I'm close to graph, it look like lines to disappear are chosen at random.
There line is
And this is if I move camera a little in angle
This is from one side of the graph:
And this is when from other:
I tried to scale down units
And also frustum = false
Whole code:
import {
WebGLRenderer,
Scene,
PerspectiveCamera,
Texture,
MeshBasicMaterial,
SphereGeometry,
Mesh,
Geometry,
Vector3,
LineBasicMaterial,
Line,
LineSegments,
BoxGeometry,
TextureLoader
} from 'three';
import * as three from 'three';
import { ViewModel, Link, Node } from './Model';
import { event } from 'd3-selection';
import * as selection from 'd3-selection';
import { drag } from 'd3-drag';
// Old module syntax
declare function require(name:String);
let OrbitControls = require('./../../../node_modules/three-orbit-controls/index')(three);
interface IView {
render():void;
}
class ViewNode {
public vector:Vector3;
public mesh:Mesh;
public node:Node;
}
export class Full3DView implements IView {
private canvas: Element;
private renderer: WebGLRenderer;
private scene: Scene;
private lineMaterial: LineBasicMaterial;
private camera: PerspectiveCamera;
private controls: any;
private nodes:ViewNode[] = [];
private lines:Geometry[] = [];
constructor(private model:ViewModel) {
this.canvas = document.querySelector('#view3d2');
this.model.onChange(() => {this.render()});
}
render(): void {
this.buildScene();
this.model.simulation.on('tick', () => this.redraw());
this.model.linkForce.distance(40);
this.model.collideForce.radius(30);
}
private buildScene() {
this.scene = new Scene();
this.camera = new PerspectiveCamera( 90, window.innerWidth/window.innerHeight, 1, 20000 );
this.renderer = new WebGLRenderer();
this.renderer.setSize( this.canvas.clientWidth, this.canvas.clientHeight );
this.canvas.appendChild( this.renderer.domElement );
this.controls = new OrbitControls( this.camera, this.renderer.domElement);
this.controls.addEventListener('change', () => {this.redraw()});
this.lineMaterial = new LineBasicMaterial({ color: 0xccff00, linewidth: 3});
let vectorIndex:Map<String, Vector3> = new Map();
let textureLoader = new TextureLoader();
this.model.nodes.forEach((node:Node) => {
this.buildNode(vectorIndex, textureLoader, node);
});
this.model.links.forEach((link:Link) => {
this.buildEdge(vectorIndex, link);
});
this.camera.position.z = 5000;
}
private buildNode(vectorIndex:Map<String, Vector3>, textureLoader:TextureLoader, node:Node) {
let material = new MeshBasicMaterial();
let geometry = new BoxGeometry( 30, 30, 30);
let mesh = new Mesh( geometry, material );
mesh.lookAt(this.camera.position);
this.scene.add( mesh );
mesh.position.set(node.x, node.y, 0);
mesh.rotation.x += 1;
vectorIndex.set(node.index, mesh.position);
this.nodes.push({
vector: mesh.position,
mesh: mesh,
node: node
});
textureLoader.load('/data/images/' + node.id + '.jpg', (texture:Texture) => {
material.map = texture;
material.needsUpdate = true;
});
}
private buildEdge(vectorIndex:Map<String, Vector3>, link:Link) {
let geometry = new Geometry();
geometry.vertices.push(
vectorIndex.get(link.source.index).copy(vectorIndex.get(link.source.index).setZ(0)),
vectorIndex.get(link.target.index).copy(vectorIndex.get(link.target.index).setZ(0))
);
geometry.computeLineDistances();
this.lines.push(geometry);
let line = new Line(geometry, this.lineMaterial);
this.scene.add(line);
}
private redraw() {
this.nodes.forEach((node:ViewNode) => {
node.vector.setX(node.node.x * 10);
node.vector.setY(node.node.y * 10);
node.mesh.lookAt(this.camera.position);
node.mesh.frustumCulled = false;
});
this.lines.forEach((line:Geometry) => {
line.verticesNeedUpdate = true;
});
this.renderer.render(this.scene, this.camera)
}
}
The actual answer I was looking for is in the comment above from WestLangley.
A possible explanation for your issue is that when you update the vertices of a geometry, you should call geometry.computeBoundingSphere(). The renderer calls it for you on the first render call, but after that, if you modify vertices, the bounding sphere is no longer correct, and you need to update it. Alternatively, you can set mesh.frustumCulled = false;
I was unable to get it working using Line object, but if I use LineSegments and push all pairs of vertices to one Geometry, it is working well.
So in function buildScene I use instead of
this.lineMaterial = new LineBasicMaterial({ color: 0xccff00, linewidth: 3});
lines
this.linesGeometry = new Geometry();
this.scene.add(new LineSegments(this.linesGeometry, new LineBasicMaterial({ color: 0xccff00, linewidth: 3})));
and then content of buildEdge is
this.linesGeometry.vertices.push(
vectorIndex.get(link.source.index).copy(vectorIndex.get(link.source.index).setZ(0)),
vectorIndex.get(link.target.index).copy(vectorIndex.get(link.target.index).setZ(0))
);
and in redraw function I just do
this.linesGeometry.verticesNeedUpdate = true;
instead of
this.lines.forEach((line:Geometry) => {
line.verticesNeedUpdate = true;
});

Categories

Resources