Gebruik de pijltjes toetsen om rond te rijden
Voor dit experiment wil ik een model leren manipuleren, en animeren op basis van parameters. Ik heb ervoor gekozen om een trekker met een flexibele frame te maken.
Kennis opdoen over manipuleren van geometries, en de moeilijkheidsgraad kunnen inschatten van dit doorvoeren met interactie.
Ik ga mij niet focussen op het maken van een (uitgebreide) model zelf. De focus ligt echt op interactie en manipulatie.
Tijdens de eerste poging heb ik geprobeerd de model direct te manipuleren middels een soort eigen lattice te maken. Ik vond een enorm leerzaam artikel; Mesh Manipulation Using Mean Values Coordinates in Three.js. In dit artikel gebruiker ze mean values om de deforms toe te passen, dit is wel wat ik nodig had, maar voor een eerste experiment wel te complex. Dit geld ook voor de andere interessante bronnen: geometry modifiers en path flow.
In de zoektocht naar een makkelijker te implementere oplossing kwam ik op de animatie mixer van THREE
.
Hiermee kun je verschillende animaties tegelijk afspelen, en zelf controle houden over de "zwaarte" waarmee je het toepast.
Daarna ben ik gaan zoeken naar hoe je animaties kan importeren in THREE
, vanuit een 3D modeling software pakket. Eerst via JSON
import, maar dat leek depricated te zijn.
Vervolgens via GLTF
, hiermee kunnen ook animaties en textures geimporteerd worden.
import 'three/examples/js/loaders/GLTFLoader';
function loadModel() {
var loader = new THREE.GLTFLoader();
loader.load('trekker.glb', function (gltf) {
var model = gltf.scene;
tractorObj = model.children[0];
scene.add( model );
});
}
Vervolgens ben ik gaan kijken hoe ik het schuintrekken kon doen met Cinema 4D. Toen kwam ik op hetzelfde principe als het artikel over mean values, de lattice deformer, perfect voor het resultaat wat ik voor ogen had. Met deze deformer kun je een kooi om je model zetten, wanneer je punten in deze kooi verplaatst, gaan alle punten in de buurt binnen de kooi ook mee.
Al snel merkte ik dat er weinig export tools voor Cinema 4d naar THREE
waren, dus ben ik geswitched naar Blender. Een gratis modelling pakket die ook gebaseerds is op OpenGl
net als webgl
(de basis van THREE
)
Elke stand die de trekker moest krijgen heb ik als animatie in Blender gemaakt.
Het importeren van deze animatie bleek lastig. Basis transformaties zoals translate en rotate werken wel. Maar modifiers niet. Een andere manier waarop het wel kan is de animatie omzetten naar keyframes (voor elke frame), dit kan met lightwave point cache. Dit heb ik geimplementeerd, maar daardoor verlies je wel de controle over de inividuele animaties.
Om dit op te lossen heb ik de animaties handmatig los geknipt met code en in de workflow van THREE gezet.
loader.load('v0-MDD-2.glb', function (gltf) {
var model = gltf.scene;
mixer = new THREE.AnimationMixer(model);
var clips = ['forwards', 'backwards', 'right', 'left'];
var stepLength = 40;
var actions = new Array(4);
for (let i = 0; i < clips.length; i++) {
actions[i] = mixer.clipAction(
createAction(i, clips[i], gltf.animations[0], stepLength)
);
}
actions[2].play();
scene.add( model );
});
function createAction(index, name, animation, step) {
let baseTrack = animation.tracks[0];
let sceneLength = animation.duration;
let frameDuration = sceneLength / 160;
let i = index + 1;
let skipStep = i * step;
let prevSkipStep = index * step;
let track = baseTrack.clone().trim(frameDuration + (frameDuration * prevSkipStep), skipStep);
if (index > 0) {
track.shift(-(frameDuration + frameDuration * prevSkipStep));
}
let clip = new THREE.AnimationClip(
name,
sceneLength / 4,
[
track
]
);
return clip;
}
Het werkte wel, maar de code was lang en moeilijk te lezen, ook het modelbestand werdt erg zwaar door alle individuele keyframes.
Dus ben opzoek gegaan naar alternatieve manieren. In een van de voorbeelden zat een gedeelte "expressions" van de robot, die leek op wat ik ook wou bereiken.
Dus ik heb onderzocht hoe dit gemaakt is. In de broncode vond ik morphTargetInfluences
en morphTargetDictionary
. Precies wat ik nodig had!
Later vond ik ook hoe dit in Blender aangemaakt en geexporteerd kon worden. Door per stand de lattice modifier toe te passen, en deze als shape key opslaan.
Deze shapekeys worden door THREE opgehaald als morphTargetDictionary
. Dit heb ik doorgevoerd.
function loadModelThingies() {
var loader = new THREE.GLTFLoader();
loader.load('trekker-morph-1-multipart.glb', function (gltf) {
var model = gltf.scene;
tractorObj = model.children[0];
wheelObjects = [model.children[1], model.children[2], model.children[3]];
var expressions = Object.keys( tractorObj.morphTargetDictionary );
var expressionFolder = gui.ddFolder('Blob');
for ( var i = 0; i < expressions.length; i++ ) {
expressionFolder.add( tractorObj.morphTargetInfluences, i, 0, 1, 0.01 ).name( expressions[ i ] );
}
scene.add( model );
});
}
Veel korter en beter te beheren. Zo kunnen er ook makkelijk meerdere standen toegevoegd worden. Deze waarden heb ik eerst in de dat.gui gezet, om te kijken hoe het manipuleren van de waardes eruit zag.
Wanneer je alleen de x en y positie van de trekker (linear) update, krijg je een erg statische animatie. Daarom is het belangrijk om met velocity te werken. Ik heb de stappen van een tutorial gevolgd om de basisprincipes onder de knie te krijgen: Implementing Velocity, Acceleration and Friction on a Canvas
// Register key events
document.addEventListener('keydown', function(e){
keys[e.which] = true;
});
document.addEventListener('keyup', function(e){
keys[e.which] = false;
});
var tractor = {
x: 0, y: 0,
vx: 0, vy: 0,
ax: 0, ay: 0,
vr: 0, ar:0,
sr: 0, r: 0,
update: function(){
if (tractorObj) {
// Set min and max for velocity calculations
this.vx = THREE.Math.clamp(this.vx, -1.0, 1.0); //float (-1) otherwise it will see it as a boolean
this.vy = THREE.Math.clamp(this.vy, -1.0, 1.0);
this.vr = THREE.Math.clamp(this.vr, -10.0, 10.0);
// Rotation from velocity to degrees
let rotation = THREE.Math.degToRad(this.vr) * 10;
// Rotate object (with custom function)
rotateObject(tractorObj.parent, 0, rotation, 0);
// Move tractor on Z axe
tractorObj.parent.translateZ(this.vx);
// Rotate the wheels
wheelObjects[0].rotation.x += (THREE.Math.degToRad(this.vx) * 10);
wheelObjects[1].rotation.x += (THREE.Math.degToRad(this.vx) * 10);
wheelObjects[2].rotation.x += (THREE.Math.degToRad(this.vx) * 10);
}
}
};
function posCalcs() {
if (keys[37]) {
tractor.ar += 0.05;
tractor.sr += 0.05;
} else if (keys[39]) {
tractor.ar -= 0.05;
tractor.sr -= 0.05;
} else {
tractor.ar = 0;
}
if(keys[38]){
tractor.ax += 0.005;
tractor.ay += 0.005;
} else if(keys[40]) {
tractor.ax -= 0.005;
tractor.ay -= 0.005;
} else {
tractor.ax = 0;
tractor.ay = 0;
}
updatePosition(tractor);
tractor.update();
}
function applyFriction(obj){
obj.vx *= friction;
obj.vy *= friction;
obj.vr *= rFriction;
}
function updatePosition(obj){
//update velocity
obj.vx += obj.ax;
obj.vy += obj.ay;
obj.vr += obj.ar;
applyFriction(obj);
//update position
obj.x += obj.vx;
obj.y += obj.vy;
obj.r += obj.vr;
}
Voor het roteren van de wielen is het wel vereist om het middelpunt van de wielen goed te hebben staan. Anders krijg je zoals hieronder.
function resetWheel() {
wheelObjects[0].geometry.center();
wheelObjects[1].geometry.center();
wheelObjects[2].geometry.center();
wheelObjects[0].position.set(3.6, 3.4, -4.8);
wheelObjects[1].position.set(-3.6, 3.9, -5.4);
wheelObjects[2].position.set(0, 2.1, 4.5);
}
Nu moeten de voorwielen alleen ook nog over de Y as draaien zodat het lijkt alsof de trekker stuurt. Helaas als je de y en z as tegelijk draait, doen ze het niet afzonderlijk, maar dan krijg je het gemiddelde. Na een lange zoektoch kwam ik erachter dat dat het GIMBAL probleem heet.
Omdat ik maar 2 assen wil draaien, kon ik dus d.m.v. euler simpel de volgorde van de assen aanpassen.
var newX = THREE.Math.degToRad(this.x) * 10; // Rolling
var newY = THREE.Math.degToRad(this.vr) * 2; // Steering
// fix gimbal problem
var euler = new THREE.Euler( newX, newY, 0, 'YXZ' );
wheelObjects[2].setRotationFromEuler(euler); // front wheels
De laatste stap was de trekker schuintrekken op basis van het rijden. De morphTargetDictionary
had ik al aangelegd.
Dus het was nog een kwestie van het toevoegen van de velocity toepassen op de morphTargetInfluences
.
if (Math.abs(this.vx) > blobbyMinSpeed) {
tractorObj.morphTargetInfluences[0] = this.vx;
tractorObj.morphTargetInfluences[2] = this.vr * 0.1;
}
Ook heb ik wat camera chase opties toegevoegd. Inspiratie van deze thread.
Vooral pragmatisch nadenken over de tools die handig kunnen zijn was hierbij moeilijk. Bij de eerste stappen (mean values en exporteren van keyframes) heb ik wel veel geleerd over animaties en manipulaties van vertexes, maar het was wel een enorme omweg. De moeilijkheid zat hem in het onderzoeken van mogelijke stappen. Omdat ik nog weinig kennis had, heb ik niet de meest rendabele keuzes gemaakt in het begin van het proces.
Mijn inzichten in de leercurve per item, los van dit specifieke usercase.