How to clone and move 3D models along different paths?

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 https://youtu.be/IDRr2XatBro. But now I want to load in more than one model and have them all move in different paths.

I took help from this tutorial 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/three@0.161.0/examples/jsm/loaders/GLTFLoader.js';
import * as SkeletonUtils from 'https://unpkg.com/three@0.161.0/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 );

}

Your code goes like this:

//3d baby model
const loader_3d = new GLTFLoader().setPath('baby_1motions/');
loader_3d.load('scene.gltf', (gltf) => ....)

//cloning babies
for (const [key, value] of Object.entries(word_and_coord)){
   ...
   babyClone = SkeletonUtils.clone(baby_global);
   ...
}

Loading assets is asynchronous process. This is, loader_3d.load only initiates the loading. As a result, you try to clone the model before it finished loading. This is just a guess by looking at the code. As you are the only one that can debug it, try to check the moment of cloning and the moment of completed loading.

As a proof of concept, here is a demo of one model with 3 clones. Each clone has its own path:

https://codepen.io/boytchev/full/YzbWNyZ

image

3 Likes

Hi @PavelBoytchev ,

Please write me if we can change and freely use parts of the Mei_Run.fbx model in another mesh creations.

Because I have splitted the parts to make an .obj model animation.

The original Mei + the license are at:

https://sketchfab.com/3d-models/mei-5478ddd14bf044e59e02bda57ec46edb

Hi @PavelBoytchev ,
Thank You for the information.
Now I know that I can´t disunite parts of this mesh for use them.