THREEjs: Objekt und Material laden

Canvas WEBGL mit 3D Animationen

Über die Jahre haben die 3D-Programme zahlreiche 3D-Formate hervorgebracht. Gleichzeitig gibt es ebenso zahlreiche Formate, die einen Austausch von Objekten zwischen den verschiedenen 3D-Anwendungen erlauben: z.B. Wavefront obj, Collada, FBX. Sie enthalten viele Elemente, die nur für die Darstellung im 3D-Programm benötigt werden.

Das jüngste Austauschformat ist glTF (Graphics Layer Transport Format) von der Kronos Group, das sich anschickt, zum Standard 3D-Format für Webseiten zu werden.

18-12-15 SITEMAP TUTORIALS

3D-Austauschformat glTF

glTF hält die Dateigröße so klein wie möglich und lädt Szenen besonders schnell. Alle Elemente der Szene werden in einer Datei exportiert – keine mtl-Datei wie beim Export der 3D-Elemente als Wavefront OBJ.

gltf-export-1
gltf-export-4
gltf-export-2
gltf-export-3
Einstellungen glTF-Export aus Blender 2.8

GLTFLoader

Der GLTFLoader ist noch kein Teil des three.js-Kerns, muss also genauso wie OrbitControls.js separat geladen werden.

<script type="module">
import * as THREE from '/threejs/r108/build/three.module.js';
import {OrbitControls} from '/threejs/r108/examples/jsm/controls/OrbitControls.js';
import {GLTFLoader} from '/threejs/r108/examples/jsm/loaders/GLTFLoader.js';

Maustaste drücken und Ziehen, um die Kamera zu rotieren; Scrollen zum Zoomen
Touchscreen: Ziehen zum Rotieren; Zoomen: zwei Finger-Geste

Parrot.glb und Flamingo.glb Modell von mrdoob / three.js auf Github

Im Script-Tag muss das Attribut type="module" gesetzt werden, damit die Scripte als Module geladen werden. Die Scripte können zwar auch auf dem klassischen Weg geladen werden, aber three.js empfiehlt das Laden als Modul seit R106.

Während beim Import als Wavefront obj neben der Modelldatei modell.obj auch noch die Materialdatei modell.mtl geladen werden muss, und Animationen nur durch eine Serie von Modell- und Materialdatei übernommen werden, ist in einer glb-Datei alles vorhanden, was für das Rendern, für Materialien und die Animation erforderlich ist.

Wenn in der glb-Datei Animationen enthalten sind, werden sie in Form eines Arrays von AnimationClips gespeichert. Die Funktion loadModells kann also gleich mehrere Modelle mit ihren jeweiligen Animationen verarbeiten.

function loadModels() {
   const loader = new GLTFLoader();
   const onLoad = ( gltf, position ) => {
      const model = gltf.scene.children[ 0 ];
      model.position.copy( position );

      const animation = gltf.animations[ 0 ];
      const mixer = new THREE.AnimationMixer( model );
      mixers.push( mixer );
      const action = mixer.clipAction( animation );
      action.play();

      scene.add( model );
   };

   // Ladefortschritt
   const onProgress = (message) => {console.log( "loading models" );};

   // Fehlermeldung an Console
   const onError = ( errorMessage ) => { console.log( errorMessage ); };

   // Modell asynchron laden. 
   const parrotPosition = new THREE.Vector3( -20, 0, 2.5 );
   loader.load( 'dist/objects/Parrot.glb', gltf => onLoad( gltf, parrotPosition ), onProgress, onError );
}

Das Laden der glb-Datei läuft asynchron ab, damit weitere Scriptanweisungen ausgeführt werden, während der onLoad auf das vollständige Laden wartet.

onLoad ist eine Callback-Funktion und wird aufgerufen, sobald das Modell fertig geladen ist.

onProgress ist ebenfalls ein Callback, das während des Ladevorgangs aufgerufen wird und wird geleert, wenn das Script wie gewünscht funktioniert.

const onProgress = () => {};

Zugriff auf Eigenschaften des importieren Objekts

Obwohl das Objekt mitsamt Texturen und Animationen in der glb-Datei importiert wird, lassen sich alle Elemente des Objekts erreichen.

castShadow   : false
children     : [] (0)
drawMode     : 0
frustumCulled: true
geometry     : BufferGeometry {id: 9, uuid: "2611772B-5811-4779-8793-93DBDB0620CE", name: "", type: "BufferGeometry", index: BufferAttribute, …}
id           : 17
layers       : Layers {mask: 1, set: function, enable: function, enableAll: function, toggle: function, …}
material     : MeshStandardMaterial {id: 14, uuid: "337C5D50-F089-4895-8F33-E1CE4BE84E40", name: "", type: "MeshStandardMaterial", fog: true, …}
matrix       : Matrix4 {elements: Array, isMatrix4: true, set: function, identity: function, clone: function, …}
matrixAutoUpdate: true
matrixWorld  : Matrix4 {elements: Array, isMatrix4: true, set: function, identity: function, clone: function, …}
matrixWorldNeedsUpdate: false
modelViewMatrix: Matrix4 {elements: Array, isMatrix4: true, set: function, identity: function, clone: function, …}
morphTargetDictionary: {flamingo_flyA_000: 0, flamingo_flyA_001: 1, flamingo_flyA_002: 2, flamingo_flyA_003: 3, flamingo_flyA_004: 4, …}
morphTargetInfluences: [0, 0, 0, 0, 0, 0.7797618723507571, 0.22023812764924292, 0, 0, 0, …] (14)
name         : "mesh_0"
normalMatrix : Matrix3 {elements: Array, isMatrix3: true, set: function, identity: function, clone: function, …}
parent       : Scene {id: 8, uuid: "2F44433C-79C1-4298-91AA-CC9EAA8ADF2E", name: "", type: "Scene", parent: null, …}
position     : Vector3 {x: -50, y: -50, z: -80, isVector3: true, set: function, …}
quaternion   : Quaternion {_x: 0, _y: 0, _z: 0, _w: 1, _onChangeCallback: function, …}
receiveShadow: false
renderOrder  : 0
rotation     : Euler {_x: 0, _y: 0, _z: 0, _order: "XYZ", _onChangeCallback: function, …}
scale        : Vector3 {x: 1, y: 1, z: 1, isVector3: true, set: function, …}
type         : "Mesh"
up           : Vector3 {x: 0, y: 1, z: 0, isVector3: true, set: function, …}
userData     : {targetNames: Array}
uuid         : "20ECB58E-2265-4209-9C3D-D988B8693A11"
visible      : true

Der gltf-Mixer steuert das Playback der Animation:

const mixer = new THREE.AnimationMixer( model );
action.setLoop( );

Die Animation nach einem Durchlauf anhalten

action.setLoop( THREE.LoopOnce );

Die Dauer der Animation bestimmen – z.b. 22 Sekunden

action.setDuration(22).play();

three.js Clock – die Stopuhr

Für die Steuerung der Animation wird die three.js Clock eingesetzt.

Das vollständige Script

Wenn kein canvas-Tag an three.js übergeben wird, erzeugt three.js das Canvas-Element selber. Ebensogut könnte als ein div benutzt werden. Für den renderer müsste dann nur eine Script-Zeile ergänzt werden: container.appendChild(renderer.domElement).

Ein direktes Canvas-Element ist besser lesbar und lässt sich flexibel an jede beliebige Stelle innerhalb des HTML-Markups einsetzen.

<canvas id="canvas"></canvas>
…
<script type="module">
import * as THREE from '/threejs/dist/threejs/build/three.module.js';
import {OrbitControls} from '/threejs/dist/threejs/examples/jsm/controls/OrbitControls.js';
import {GLTFLoader}    from '/threejs/dist/threejs/examples/jsm/loaders/GLTFLoader.js';
  
const canvas = document.querySelector( '#canvas' );
let renderer = new THREE.WebGLRenderer({canvas});

const camera = new THREE.PerspectiveCamera( 86, 2, 1, 100 );
camera.position.set( 60, 1.5, 6.5 );

const controls = new OrbitControls( camera, canvas );
const scene = new THREE.Scene();

const mixers = [];
const clock = new THREE.Clock();

function main() {

   scene.background = new THREE.Color('skyblue');

   {
      const ambientLight = new THREE.HemisphereLight( 0xddeeff, 0x0f0e0d, 5 );
      const mainLight = new THREE.DirectionalLight( 0xffffff, 5 );
      mainLight.position.set( 10, 10, 10 );
      scene.add( ambientLight, mainLight );
   }
   
   {
      // WebGLRenderer Breite und Höhe setzen
      renderer.setSize( canvas.clientWidth, canvas.clientHeight );
      renderer.setPixelRatio( window.devicePixelRatio );
      renderer.gammaFactor = 2.2;
      renderer.gammaOutput = true;
      renderer.physicallyCorrectLights = true;
   }
   
   loadModels();

   function render() {
      renderer.render( scene, camera );
   }
   
   function update() {
      const delta = clock.getDelta();
      for ( const mixer of mixers ) {
         mixer.update( delta );
      }
   }

   function onWindowResize() {
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      
      // Update für das Frustum der Kamera
      camera.updateProjectionMatrix();
      renderer.setSize( canvas.clientWidth, canvas.clientHeight );
   }

   window.addEventListener( 'resize', onWindowResize );
   
   renderer.setAnimationLoop( () => {
      update();
      render();
   });

}

function loadModels() {
   const loader = new GLTFLoader();
   const onLoad = ( gltf, position ) => {
      const model = gltf.scene.children[ 0 ];
      model.position.copy( position );

      const animation = gltf.animations[ 0 ];
      const mixer = new THREE.AnimationMixer( model );
      mixers.push( mixer );
      const action = mixer.clipAction( animation );
      action.play();

      scene.add( model );
   };

   // Ladefortschritt
   const onProgress = (message) => {console.log( "loading models" );};

   // Fehlermeldung an Conole
   const onError = ( errorMessage ) => { console.log( errorMessage ); };

   // Modell asynchron laden. 
   const parrotPosition = new THREE.Vector3( -20, 0, 2.5 );
   loader.load( 'dist/objects/Parrot.glb', gltf => onLoad( gltf, parrotPosition ), onProgress, onError );
}

main();
</script>