Yodacon Ship Pack · Tutorial Note 5 · October 2026
Pre-Rotated Sprites in 3-D: How the three.js Example Uses the Pack
Abstract—The three.js example shows a pack ship as a textured plane and as a billboard. One texture of sheet-alpha.png serves all 36 frames by moving its UV window; for the billboard the frame also accounts for the camera's bearing. This note follows the page's source.
Index Terms—three.js, WebGL, texture atlases, billboards, pixel art.
I. Introduction
The three.js example puts a pre-rotated 2-D sprite into a 3-D scene two ways: as a quad lying on a grid
(?mode=plane) and as a camera-facing billboard you orbit (?mode=billboard). Both use
one texture whose UV window is moved to the wanted frame. three.js 0.160.0 is pinned through an import map
(index.html:24).
II. Assets Used
| Asset | Read at | Used for |
|---|---|---|
ship.json | index.html:49 | Frame size, grid, stats, name |
sheet-alpha.png | index.html:106 | One texture for all 36 frames |
frameIndex() | index.html:45 | Heading to frame |
III. The Sheet as a Texture
The texture is loaded once and set up for pixel art: nearest-neighbour filtering both ways and no mipmaps
(Listing 1). repeat shrinks the visible UV range to one cell; offset moves it.
three.js flips images vertically on upload, so v = 0 is the sheet's bottom row. For frame
f in a sheet of C columns and R rows:
u₀ = (f mod C) / C, v₀ = 1 − (⌊f / C⌋ + 1) / R(1)
index.html:106–114106const texture = await new THREE.TextureLoader().loadAsync(ship.urls.sheet);107texture.colorSpace = THREE.SRGBColorSpace;108texture.magFilter = texture.minFilter = THREE.NearestFilter; // pixel art: no blur109texture.generateMipmaps = false;110texture.repeat.set(1 / cols, 1 / rows);111function setFrame(i) {112 texture.offset.set((i % cols) / cols, 1 - (Math.floor(i / cols) + 1) / rows);113}114const material = { map: texture, transparent: true, alphaTest: 0.5 }; // alphaTest: crisp edges, no sorting trouble
alphaTest: 0.5 discards transparent texels outright, which keeps edges crisp and avoids
transparency sorting entirely.
IV. Mode 1: A Plane on the Grid
A PlaneGeometry sized to the frame in world units is laid flat, its texture's top pointing to
−Z, “north” (Listing 2). Each tick picks the frame for the heading and thrusts along that frame's
direction, (sin a, 0, −cos a) in world space, so motion matches the picture
(index.html:140).
index.html:128–147128const plane = new THREE.Mesh(new THREE.PlaneGeometry(fw * SIZE, fh * SIZE), new THREE.MeshBasicMaterial(material));129plane.rotation.x = -Math.PI / 2;130plane.position.y = 1;⋮139 const frame = frameIndex(heading);⋮147 setFrame(frame);
V. Mode 2: A Billboard
A THREE.Sprite always faces the camera, so it must show the ship as seen from the
camera. If the ship's true heading is θ and the camera's bearing around it is φ, the frame to
show is
f = frameIndex(θ + φ)(2)
which the loop computes from the camera position each frame (Listing 3). Orbit the camera with the ship still and the sprite turns; the green arrow shows that the ship itself has not.
index.html:178–180178const phi = THREE.MathUtils.radToDeg(Math.atan2(camera.position.x - sprite.position.x, camera.position.z - sprite.position.z));179const seen = heading + phi;180setFrame(frameIndex(seen));
VI. Using It in Your Project
- Load the sheet with
TextureLoader; setNearestFilter, no mipmaps, sRGB. - Set
repeatto (1/C, 1/R) and moveoffsetwith (1). - For many ships, clone the texture per ship (
texture.clone()shares the image) so each has its own offset. - For billboards, add the camera bearing as in (2).
The Fleet Flyby and Hangar projects scale this up to every ship and to a turntable [2].
VII. Conclusion
In 3-D the pack's convention survives intact: one texture, a UV window chosen by Equation (1), and for billboards one extra angle. The sprites keep their 1997 pixels while the scene around them is fully 3-D.
References
- Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
- Yodacon Ship Pack, “Sample projects,” projects/.
- three.js authors, three.js r160 (0.160.0), 2023. [Online]. Available: threejs.org/docs