This library can be used to load ThreeJS assets (models, textures, audio, etc) and store them in an atom.
shadow-cljs.ednnpm:npm install -S troika-three-text howler three
To use this library, we need to define our "asset tree" which mirrors the layout of the directory structure where our asset files are served. This makes it easy to keep our files organized on disk and update our application whenever we need to load a new asset file.
For example, let's say our assets are under a directory structure like this:
- assets
- models
- alien.glb
- robot.glb
- textures
- tile.png
- particle_soft.png
- audio
- sfx
- menu_accept.ogg
- menu_decline.ogg
- music
- main_menu.ogg
In our application, we'd define our asset tree and load it like this:
(ns my.app
(:require [threeagent.assets :as assets]))
(defonce asset-db (atom {}))
(def asset-tree
[["assets"
["models" {:loader assets/model-loader}
["alien.glb" :model/alien {}]
["robot.glb" :model/robot {}]]
["textures" {:loader assets/texture-loader}
["tile.png" :texture/tile {}]
["particle_soft.png" :texture/particle-soft {}]]
["audio" {:loader assets/audio-howler-loader} ;; Uses Howler.js to load audio
["sfx"
["menu_accept.ogg" :sfx/menu-accept {}]
["menu_decline.ogg" :sfx/menu-decline {}]]
["music"
["main_menu.ogg" :music/main-menu {}]]]]])
(defn load-assets!
"Loads all of the asset files, called during app initialization.
Returns a Promise that resolves when all assets have been loaded"
[]
(assets/load! asset-db asset-tree))
After the promise returned from assets/load! completes, we can fetch assets from our asset-db using its key:
(ns my.app.scene
(:require [my.app :refer [asset-db]]))
(defn some-threeagent-component []
[:object
[:box {:material {:map (:texture/tile @asset-db)}}]])
For faster loading, you can bundle your assets into a zip file and load them with assets/load-zip!. This downloads a single zip file, extracts it in memory, and loads assets from the extracted contents.
(ns my.app
(:require [threeagent.assets :as assets]))
(defonce asset-db (atom {}))
;; The asset tree paths should match the structure inside the zip
(def asset-tree
[["models" {:loader assets/model-loader}
["alien.glb" :model/alien {}]
["robot.glb" :model/robot {}]]
["textures" {:loader assets/texture-loader}
["tile.png" :texture/tile {}]]])
(defn load-assets! []
(assets/load-zip! asset-db "./assets.zip" asset-tree))
If your zip file has a root folder (e.g., the zip contains assets/models/alien.glb instead of models/alien.glb), use the :base-path option:
(assets/load-zip! asset-db "./assets.zip" asset-tree {:base-path "assets"})
This library comes with loaders for common types of assets: models, textures, audio, and fonts. These loaders are wrappers around the standard ThreeJS loaders.
The threeagent.assets/model-loader is used to load 3D models. It currently supports GLTF and FBX files using the GLTFLoader and FBXLoader provided by ThreeJS.
Optionally, this loader can create a pool for each loaded model. This is useful when you need to add multiple copies of a model to your scene.
Example:
[["assets"
["models" {:loader assets/model-loader}
;; Load our alien.glb file, and create a pool with 5 copies of the model:
["alien.glb" :model/alien {:pool-size 5}]
;; Load our robot.fbx file, and set the scale of the loaded model to [10, 10, 10]:
["robot.fbx" :model/robot {:scale 10}]]]]
When you define a :pool-size, you must use the provided threeagent.assets.pool namespace functions to claim/return models from/to the pool.
For example:
(ns my.app.scene
(:require [my.app :refer [asset-db]]
[threeagent.assets.pool :as pool]))
(defn my-component []
(let [model-pool (:model/alien @asset-db)
model (pool/claim! model-pool)]
[:object
^{:on-removed #(pool/return! model-pool model)} ;; Returns the model to the pool when this object is removed from the scene
[:instance {:object model}]]))
It is recommended to define a custom Threeagent IEntityType specifically for dealing with pooled models. For example:
(ns my-app.model-entity-type
(:require [my.app :refer [asset-db]]
[threeagent.assets.pool :as pool]
[threeagent.entity :refer [IEntityType]]))
(deftype ModelEntity []
IEntityType
(create [_ _ {:keys [model-key]}]
(let [model-pool (get @asset-db model-key)
model (pool/claim! model-pool)]
model))
(destroy! [_ _ ^three/Object3D obj {:keys [model-key]}]
(let [model-pool (get @asset-db model-key)]
(pool/return! model-pool obj))))
The threeagent.assets/audio-howler-loader is used to load audio files as Howler.js Howl instances.
We can define the options used to construct the Howl instance via the asset properties map. For example:
["audio" {:loader assets/audio-howler-loader}
["impacts.ogg" :sfx/impacts {:sprite {"1" [0 500] ;; Defined as [offset duration]
"2" [500 200]}
:volume 0.2}]
["music.ogg" :music/main-menu {:loop true}]]
;; Usage
;; -- play music
(.play (:music/main-menu @asset-db))
;; -- play sprite
(.play (:sfx/impacts @asset-db) "1")
The threeagent.assets/font-troika-loader is used to preload fonts for usage with the troika-three-text library.
The loaded value will be the font file's path, which can be set as the font property on a Troika Text instance:
["fonts" {:loader assets/font-troika-loader}
["menu_font.ttf" :font/main-menu {:characters ["a" "b" "c" "d" "1" "2" "3"]}]]
;; Usage
(let [text (troika/Text.)
font (:font/main-menu @asset-db)]
(set! (.-font text) font)
(set! (.-text text) "abc")
(.sync text)
(.add my-threejs-scene text))
The threeagent.assets/texture-loader is used to load ThreeJS Textures.
It uses the default ThreeJS TextureLoader to load the texture file.
We can configure the loaded Texture instance using the configuration map:
["textures" {:loader assets/texture-loader}
["grid.png" :texture/grid {:repeat {:x 4
:y 4}
:rotation 0.4
:wrap-s three/RepeatWrapping
:wrap-t three/RepeatWrapping
:premultiply-alpha true}]]
The threeagent.assets/data-loader is used to load data files (JSON and EDN) and parse them as Clojure data structures.
File type is detected by extension:
.json files are parsed with JSON.parse and converted to Clojure data.edn files are parsed with clojure.edn/read-stringBy default, all string keys in maps are converted to keywords. This can be disabled with the :keywordize-keys option:
["data" {:loader assets/data-loader}
;; JSON file - keys will be keywordized by default
["config.json" :data/config {}]
;; EDN file
["levels.edn" :data/levels {}]
;; Keep string keys (don't keywordize)
["external.json" :data/external {:keywordize-keys false}]]
;; Usage
(let [config (:data/config @asset-db)]
(println (:name config))
(println (get-in config [:settings :debug])))
npm ci
npm run test-once
For watch mode during development:
npm run watch-test
Releases are managed through CircleCI and deployed to Clojars.
Push to the release branch:
git checkout -b release
git push origin release
Wait for tests to pass in CircleCI
Approve the release in the CircleCI UI (there's a manual hold step)
The release job will automatically:
vX.X.XmainSnapshots are automatically deployed to Clojars on every push to main.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |