I have loaded an animated 3D model of a baby in my Three.js scene. I can update its position in my animate loop and it moves along the path I want. But now I want to load in more than one model and have them all move in different paths.
I looked up tutorials like this and their example but I can’t adapt the code correctly to my project. I don’t see any multiple instances/clones of the babies let alone seeing them animated + moving along a path (sc here). How do I add multiple animated models but also update their position in the animate() loop to have them move along different paths?
My original code when just one animated model loads and moves on a path:
import * as THREE from 'three';
import { OrbitControls } from 'OrbitControls';
import { FontLoader } from 'FontLoader';
import {GLTFLoader} from 'https://unpkg.com/[email protected]/examples/jsm/loaders/GLTFLoader.js';
import * as SkeletonUtils from 'https://unpkg.com/[email protected]/examples/jsm/utils/SkeletonUtils.js';
//see three.js version
//console.log(THREE.REVISION);
//All titles; cleaned scraped datas
const sentences = [
"AI: Its nature and future", "Nature and scope of AI techniques", "Advancing mathematics by guiding human intuition with AI", "Cooperative AI: machines must learn to find common ground", ...... "A comprehensive survey of ai-generated content (aigc): A history of generative ai from gan to chatgpt"
];
//dict obj of all unique words (occuring more than once) across all titles
const uniq_words = {
"ai": 218,
"its": 11,
"nature": 16,
....
"making": 2,
"ultrafiltration": 2
};
//stores every unique word and the coordinates of all titles containing that word
const word_and_coord = {};
//3 basic needs to display aanything in three js
let camera, scene, renderer;
//stores each title temporarily
let message;
//stores every title and its world position/coordinate/Vector3 value in the scene
const title_and_coord = {};
//adding a camera
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(0, 0, 1500);
//adding a scene
scene = new THREE.Scene();
//don't change anything below because it messes resizing
//not even spacing or formatting
renderer = new THREE.WebGLRenderer( {antialias: true} );
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
//controls.update();
controls.addEventListener('change', animate);
window.addEventListener('resize', onWindowResize);
const light = new THREE.AmbientLight( 0x404040 ); // soft white light
scene.add( light );
//invisible path to follow for the baby
let curve = null;
//creating a font and using it as the geometry
const loader = new FontLoader();
loader.load('./fonts/Montserrat_Regular.json', function (font){
const color = 0xFFF1DF;
const matLite = new THREE.MeshBasicMaterial({
color: color,
opacity: 1.0,
side: THREE.DoubleSide
});
for (let i = 0; i < sentences.length; i++) {
var title_coord = new THREE.Vector3();
message = sentences[i];
const shapes = font.generateShapes(message, 100);
const geometry = new THREE.ShapeGeometry(shapes);
//generate a 1 or -1
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var plusOrMinus2 = Math.round(Math.random()) * 2 - 1;
var title = new THREE.Mesh(geometry, matLite);
title.position.x = i*90*plusOrMinus*Math.random();
title.position.y = plusOrMinus2 * (300 + ((Math.random()*100)) + (Math.random()*1000));
//z is very spread out for this combination of values
title.position.z = ( 700 + ((Math.random()*100) + 1) + (i*Math.random()*1000) );
//plusOrMinus * ( 900 + ((Math.random()*10) + 0) + (Math.random()*100) + (Math.random()*1000) );
//title.position.z = plusOrMinus2 * (900 + ((Math.random()*10) + 0) + (Math.random()*100) + (Math.random()*1000));
//title.position.y = plusOrMinus2 * ( 900 + ((Math.random()*10) + 0) + (Math.random()*100) + (Math.random()*1000) );
//title.rotateY(Math.random() * 1.1 * 3.14 * plusOrMinus2);
title.scale.setScalar(0.5)
scene.add(title);
title_and_coord[message] = title.getWorldPosition(title_coord);
//printing each title's coord
//console.log(title.getWorldPosition(title_coord));
//title loop ends here
}
/*
Rendering lines across titles with shared words
PSEUDOCODE:
- Loop through each unique word in uniq_words dictionary (604 at present)
- Loop through all the titles (508 at present)
- if the word appears in the title
- Make a new dict/add to a dict
where the key is the word and value is the world coordinates of the title
for example:
{
"ai" : [ {242.34, 234.989, 8756.21}, {}, ...],
"its" : [ {922.34, 834.989, 3177.21}, Vector3, Vector3, ....],
.....
}
Once dict is complete, loop through it and draw lines intersecting at all those coordinates
*/
for (let word in uniq_words) {
var shared_coords = [];
//console.log(title_and_coord)
Object.keys(title_and_coord).forEach(key => {
//console.log(title_and_coord[key]);
const lwrcase = key.toLowerCase();
if (lwrcase.includes(word)){
//append the coord of title to list/array initialized before the loop
shared_coords.push(title_and_coord[key]);
}
});
word_and_coord[word] = shared_coords;
}
//console.log(word_and_coord);
//setting up the main associations/mappings/lines
const material = new THREE.LineDashedMaterial({
color: 0xFF8700,
dashSize: 20,
gapSize: 7.5,
lineWidth: 0.1
});
//loop through every word and its matching coordinates
for (const [key, value] of Object.entries(word_and_coord)) {
//console.dir(`Key: ${key}, Value: ${value}`);
//.setFromPoints(x) needs x to be a list/array of Vec3 values
//value of word_and_coord obj is already an array of Vec3 values
const geometry = new THREE.BufferGeometry().setFromPoints(value);
const line = new THREE.Line( geometry, material );
//to have dashes on a line you have to call .computeLineDistance() on your geometry
line.computeLineDistances();
scene.add(line);
}
//console.log(word_and_coord);
});
let mixer = null;
let elapsedTime = 0; // To keep track of time
const pathDuration = 10000; // Duration in seconds for one complete loop
let baby = null;
//3d baby model
const loader_3d = new GLTFLoader().setPath('baby_1motions/');
loader_3d.load('scene.gltf', (gltf) => {
baby = gltf.scene;
scene.add(baby);
var light = new THREE.AmbientLight(0xffffff);
scene.add(light);
//console.log(gltf.animations);
mixer = new THREE.AnimationMixer(baby);
const action = mixer.clipAction(gltf.animations[0]);
action.play();
})
const clock = new THREE.Clock();
//render the entire scene
function animate() {
//console.log(word_and_coord);
const delta = clock.getDelta();
elapsedTime += delta;
if(mixer){
mixer.update(delta);
}
if (baby){
const t = (elapsedTime % pathDuration) / pathDuration; // Normalize to [0, 1]
curve = new THREE.CatmullRomCurve3(word_and_coord["ai"], true);
const position = curve.getPoint(t);
baby.position.copy(position);
}
renderer.render( scene, camera );
}
renderer.setAnimationLoop(animate);
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
animate();
}
These were the changes made when adding more models (comes in after fontloader block ends):
let mixer = null;
let elapsedTime = 0; // To keep track of time
const pathDuration = 10000; // Duration in seconds for one complete loop
let baby = null;
let baby_global;
let baby_gltf_global;
let clips;
//3d baby model
const loader_3d = new GLTFLoader().setPath('baby_1motions/');
loader_3d.load('scene.gltf', (gltf) => {
baby = gltf.scene;
baby_global = gltf.scene;
baby_gltf_global = gltf;
clips = gltf.animations;
})
//cloning babies
const objects = [];
const mixers = [];
let babyClone = null;
for (const [key, value] of Object.entries(word_and_coord)){
babyClone = SkeletonUtils.clone(baby_global);
babyClone.position.copy(value, true);
scene.add(babyClone);
var light2 = new THREE.AmbientLight(0xffffff);
scene.add(light2);
objects.push(babyClone);
const mixer2 = new THREE.AnimationMixer(babyClone);
const clip2 = THREE.AnimationClip.findByName(clips, 'crawling_mixamo');
const action2 = mixer2.clipAction(clip2);
action2.play();
mixers.push(mixer2);
}
const clock = new THREE.Clock();
//const timer = new Timer();
//render the entire scene
function animate() {
//console.log(word_and_coord);
const delta = clock.getDelta();
elapsedTime += delta;
if (mixers){
mixers.forEach(function(mixer) {
mixer.update(delta);
});
}
if (babyClone){
for (const [key, value] of Object.entries(word_and_coord)){
curve_new = new THREE.CatmullRomCurve3(value, true);
const position3 = curve_new.getPoint(t);
babyClone.position.copy(position3);
}
}
renderer.render( scene, camera );
}
P.S. I already have the Vec3 coordinates for making the different paths – they are stored as values in a dict object called word_and_coord
.
Sorry for the messy code – still a beginner figuring this stuff out!
Iti Gupta is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.