Skip to content

GLTF DRACO

Video Lecture

DRACO Loader DRACO Loader

Description

The DRACO loader is used to load geometry compressed with the Draco library.

Draco is an open source library for compressing and decompressing 3D meshes and point clouds.

glTF files can also be compressed using the DRACO library, and they can also be loaded using the glTF loader.

We can configure the glTF loader to use the DRACOLoader to decompress the file in such cases.

In this lesson, we will compress the Eve model from the last lesson using the glTF Transform CLI tool.

<>

Install it

npm install --global @gltf-transform/cli

List the usage help

gltf-transform --help

Now compress your model using the most common optimizations.

gltf-transform optimize <input> <output> --compress draco --texture-compress webp

If using PowerShell, add the .cmd suffix.

gltf-transform.cmd optimize <input> <output> --compress draco --texture-compress webp

Example usage for the file named eve$@walk.glb.

gltf-transform.cmd optimize '.\public\models\eve$@walk.glb' '.\public\models\eve$@walk_compressed.glb' --compress draco --texture-compress webp

In our script, we can import the DRACOLoader class.

import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'

We then instantiate using

const draco = new DRACOLoader()

The DRACOLoader, will then create a worker process that imports and executes the actual decoder scripts.

Since the Draco scripts will run in their own worker process, they cannot be bundled. They will need to be loaded into the web browser at runtime.

We can either direct the web browser to download the worker process components from a CDN, or host them ourselves somewhere under the ./public folder.

Using a CDN,

draco.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/')

or hosting locally,

dracoLoader.setDecoderPath('jsm/libs/draco/') // loading from own webserver

If hosting locally, be sure to copy all the files from ./node_modules/three/examples/jsm/libs/draco/ to ./public/jsm/libs/draco/.

Now a Caveat, compressing a file doesn't necessarily mean that the file will be presented in the scene faster. While compressed data can result in a significantly smaller file size, the users web browser will use more CPU will and time while decoding the file. Not only that, the extra components used by the Draco loader worker process to decompress, are also needed to be downloaded at runtime.

See below example showing that the compressed file appears later in the scene than the uncompressed version.

All files and applications are different, you will need to compare whether using Draco compression or not, will benefit your application.

Compressed (DRACO)
<>
Uncompressed
<>

Resources

The 3D model and animations used in this lesson can easily be downloaded from Mixamo and converted using Blender. If you don't want to download and convert, then you can download the converted files using the link below. Extract the contents of the zip file into your ./public/models/ folder.

gltf-draco.zip

Lesson Script

./src/main.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
//import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import Stats from 'three/addons/libs/stats.module.js'

class CharacterController {
  keyMap: { [key: string]: boolean } = {}
  wait = false
  animationActions: { [key: string]: THREE.AnimationAction }
  activeAction = ''
  speed = 0

  constructor(animationActions: { [key: string]: THREE.AnimationAction }) {
    this.animationActions = animationActions
    document.addEventListener('keydown', this.onDocumentKey)
    document.addEventListener('keyup', this.onDocumentKey)
  }

  onDocumentKey = (e: KeyboardEvent) => {
    this.keyMap[e.code] = e.type === 'keydown'
  }

  dispose() {
    document.removeEventListener('keydown', this.onDocumentKey)
    document.removeEventListener('keyup', this.onDocumentKey)
  }

  setAction(action: string) {
    if (this.activeAction != action) {
      this.animationActions[this.activeAction].fadeOut(0.25)
      this.animationActions[action].reset().fadeIn(0.25).play()
      this.activeAction = action

      switch (action) {
        case 'walk':
          this.speed = 1
          break
        case 'run':
        case 'jump':
          this.speed = 4
          break
        case 'pose':
        case 'idle':
          this.speed = 0
          break
      }
    }
  }

  update() {
    if (!this.wait) {
      let actionAssigned = false

      if (this.keyMap['Space']) {
        this.setAction('jump')
        actionAssigned = true
        this.wait = true // blocks further actions until jump is finished
        setTimeout(() => (this.wait = false), 1000)
      }

      if (!actionAssigned && this.keyMap['KeyW'] && this.keyMap['ShiftLeft']) {
        this.setAction('run')
        actionAssigned = true
      }

      if (!actionAssigned && this.keyMap['KeyW']) {
        this.setAction('walk')
        actionAssigned = true
      }

      if (!actionAssigned && this.keyMap['KeyQ']) {
        this.setAction('pose')
        actionAssigned = true
      }

      !actionAssigned && this.setAction('idle')
    }
  }
}

class Grid {
  gridHelper = new THREE.GridHelper(100, 100)
  speed = 0

  constructor(scene: THREE.Scene) {
    scene.add(this.gridHelper)
  }

  lerp(from: number, to: number, speed: number) {
    const amount = (1 - speed) * from + speed * to
    return Math.abs(from - to) < 0.001 ? to : amount
  }

  update(delta: number, toSpeed: number) {
    this.speed = this.lerp(this.speed, toSpeed, delta * 10)
    this.gridHelper.position.z -= this.speed * delta
    this.gridHelper.position.z = this.gridHelper.position.z % 10
  }
}

const scene = new THREE.Scene()

new RGBELoader().load('img/venice_sunset_1k.hdr', (texture) => {
  texture.mapping = THREE.EquirectangularReflectionMapping
  scene.environment = texture
  scene.background = texture
  scene.backgroundBlurriness = 1
})

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(0.1, 1, 1)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.target.set(0, 0.75, 0)

const stats = new Stats()
document.body.appendChild(stats.dom)

let mixer: THREE.AnimationMixer
let animationActions: { [key: string]: THREE.AnimationAction } = {}

const characterController = new CharacterController(animationActions)
const grid = new Grid(scene)

//const dracoLoader = new DRACOLoader()
//dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/') // loading from a CDN
//dracoLoader.setDecoderPath('jsm/libs/draco/') // loading from own webserver

const glTFLoader = new GLTFLoader()
//glTFLoader.setDRACOLoader(dracoLoader)

glTFLoader.load('models/eve$@walk_compressed.glb', (gltf) => {
  mixer = new THREE.AnimationMixer(gltf.scene)

  mixer.clipAction(gltf.animations[0]).play()

  scene.add(gltf.scene)
})

// async function loadEve() {
//   const [eve, idle, run, jump, pose] = await Promise.all([
//     glTFLoader.loadAsync('models/eve$@walk_compressed.glb'),
//     glTFLoader.loadAsync('models/eve@idle.glb'),
//     glTFLoader.loadAsync('models/eve@run.glb'),
//     glTFLoader.loadAsync('models/eve@jump.glb'),
//     glTFLoader.loadAsync('models/eve@pose.glb')
//   ])

//   mixer = new THREE.AnimationMixer(eve.scene)

//   animationActions['idle'] = mixer.clipAction(idle.animations[0])
//   animationActions['walk'] = mixer.clipAction(eve.animations[0])
//   animationActions['run'] = mixer.clipAction(run.animations[0])
//   animationActions['jump'] = mixer.clipAction(jump.animations[0])
//   animationActions['pose'] = mixer.clipAction(pose.animations[0])

//   animationActions['idle'].play()
//   characterController.activeAction = 'idle'

//   scene.add(eve.scene)
// }
// await loadEve()

const clock = new THREE.Clock()
let delta = 0

function animate() {
  requestAnimationFrame(animate)

  delta = clock.getDelta()

  controls.update()

  //characterController.update()

  mixer && mixer.update(delta)

  grid.update(delta, characterController.speed)

  renderer.render(scene, camera)

  stats.update()
}

animate()

Troubleshooting

Q. Error : gltf-transform.ps1 cannot be loaded because running scripts is disabled on this system

You try to run gltf-transform from the command line, but you get the error,

gltf-transform.ps1 cannot be loaded because running scripts is disabled on this system

A. You are using PowerShell. You can add the .cmd suffix to the command.

E.g.,

gltf-transform.cmd --help

glTF-Transform

DRACO 3D Data Compression

DRACOLoader (Official Documentation)

WebAssembly (MDN)

WebAssembly (Official)

Comments