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);
}
);
Related
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.
Hello guys!
Been doing some experiments with three.js, managed to get a 70mb model down to 10 MB using draco, however I am not really familiar with Draco and I haven't been able to find a good example on how to use Draco loader together with GLTF Loader, can anyone show me a good example or check my code to see which things I have to change to use it and load the compressed model? TYSM!
import * as THREE from '../build/three.module.js';
import Stats from './jsm/libs/stats.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
let camera, scene, renderer, stats;
var raycaster, mouse = { x : 0, y : 0 };
var objects = [];
var selectedObject;
init();
animate();
function init() {
const container = document.createElement( 'div' );
document.body.appendChild( container );
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
camera.position.set( - 1.8, 0.6, 2.7 );
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xa0a0a0 );
raycaster = new THREE.Raycaster();
const hemiLight = new THREE.HemisphereLight( 0xa0a0a0, 0xa0a0a0, 2);
scene.add(hemiLight);
// model
const loader = new GLTFLoader().setPath( 'models/fbx/lowlight3_out/' );
loader.load( 'lowlight3.gltf', function ( gltf ) {
gltf.scene.traverse( function ( child ) {
if ( child.isMesh ) {
child.castShadow = true;
child.receiveShadow = true;
}
} );
scene.add( gltf.scene );
} );
Guys just managed to get it working, just add to add Draco by following the three.js default example:
Add
import { DRACOLoader } from './jsm/loaders/DRACOLoader.js';
&
const loader = new GLTFLoader().setPath( 'models/fbx/compressed/' );
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( './js/libs/draco/' );
loader.setDRACOLoader( dracoLoader );
For anyone having the same issue, just take a look to this example:
https://threejs.org/docs/#examples/en/loaders/GLTFLoader
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()
I hope I'm not asking an obviously noob question, but I noticed on the three.js official examples, PointerLockControls.js allows for mouse pointer lock AND WASD keys navigation.
I've managed to get the mouse pointer to lock, but I am struggling to get the WASD keys to do anything.
Perhaps this is not the .js script I need to begin with?
I'm still very new to this, but I feel my file is so close! Any help would be appreciated.
this is my code
import * as THREE from './three.js-master/build/three.module.js'
import {GLTFLoader} from './three.js-master/examples/jsm/loaders/GLTFLoader.js'
import {PointerLockControls} from './three.js-master/examples/jsm/controls/PointerLockControls.js'
if (typeof window !== 'undefined' && typeof window.THREE === 'object') {
window.THREE.SimpleFPControls = SimpleFPControls;
}
const canvas = document.querySelector('.webgl')
const scene = new THREE.Scene()
const loader = new GLTFLoader
loader.load('assets/untitled.glb', function(glb){
console.log(glb)
const root = glb.scene;
root.scale.set(0.01,0.01,0.01)
scene.add(root);
}, function(xhr){
console.log((xhr.loaded/xhr.total * 100) + "% loaded")
}, function(error){
console.log('An error occured')
})
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(0,0,2)
scene.add(light)
// const geometry = new THREE.BoxGeometry(1,1,1)
//const material = new THREE.MeshBasicMaterial({
// color: 0x00ff00
//})
//const boxMesh = new THREE.Mesh(geometry,material)
//scene.add(boxMesh)
//BOILER PLATE CODE
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const camera = new THREE.PerspectiveCamera(75, sizes.width/sizes.height, 0.1, 100)
camera.position.set(0,1,10)
camera.lookAt(0, 0, 0);
scene.add(camera)
//add document.body to PointerLockControls constructor
let fpsControls = new PointerLockControls( camera , document.body );
//add event listener to your document.body
document.body.addEventListener( 'click', function () {
//lock mouse on screen
fpsControls.lock();
}, false );
const renderer = new THREE.WebGLRenderer({
canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.shadowMap.enabled = true
renderer.outputEncoding = true
document.body.appendChild(renderer.domElement)
function animate(){
requestAnimationFrame(animate)
renderer.render(scene,camera)
}
const controls = new PointerLockControls(camera, document.bodybody);
animate()
PointerLockControls only adds event listeners to pointerlockchange, pointerlockerror and mousemove. So it handles the mouse capturing and applies the mouse movements to the camera in order to rotate it.
However, the class adds not key event listeners. If you closely study the source code of official example, you will see that these event listeners are implemented on application level.
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