348 lines
12 KiB
Text
348 lines
12 KiB
Text
== Creating Assets
|
|
|
|
JoeyLib does not load PNGs, WAVs or MODs at run time. Every asset is converted
|
|
on the build host into a compact binary the target can use directly, which is
|
|
what keeps loading fast and the runtime small. This chapter covers each asset
|
|
type: what you author, which tool bakes it, and which API call loads it.
|
|
|
|
=== The Pipeline at a Glance
|
|
|
|
[cols="2,2,2,3", options="header"]
|
|
|===
|
|
|You author |Tool |Baked file |Loaded by
|
|
|
|
|Indexed PNG tile sheet
|
|
|`assetbake.py --type tile`
|
|
|`.tbk`
|
|
|`jlTileBankLoad`, `jlTileBankLoadToSurface`
|
|
|
|
|Indexed PNG sprite sheet
|
|
|`assetbake.py --type sprite`
|
|
|`.spr`
|
|
|`jlSpriteBankLoad`
|
|
|
|
|A `.spr`
|
|
|`spritebake`
|
|
|`.spc`
|
|
|`jlSpriteBankLoadPrecompiled`
|
|
|
|
|Text note notation
|
|
|`songbake.py`
|
|
|`.jym` or a C header
|
|
|`jlMusicPlay`
|
|
|
|
|Protracker `.MOD`
|
|
|`joeymod`
|
|
|`.mod`, `.amod`, `.ntp`
|
|
|`jlAudioPlayMod`
|
|
|
|
|Raw signed 8-bit PCM
|
|
|(none -- copied)
|
|
|`.sfx`
|
|
|`jlAudioPlaySfx`
|
|
|===
|
|
|
|
Baked assets are staged into `DATA/` beside the executable, and the program
|
|
opens them with bare names.
|
|
|
|
=== Graphics: Tiles and Sprites
|
|
|
|
==== Authoring the PNG
|
|
|
|
Both tile and sprite sheets start as an *indexed* PNG with strict rules:
|
|
|
|
* Mode `P` (indexed / palette), *not* RGB or RGBA.
|
|
* At most *16 colours*.
|
|
* Width and height both *multiples of 8*.
|
|
* Palette entry 0 is the transparent / background colour. Remember the library
|
|
forces it to black on screen.
|
|
|
|
Any decent pixel editor can produce this. In GIMP: *Image > Mode > Indexed*,
|
|
"Generate optimum palette" with maximum 16 colours, and no dithering --
|
|
dithering invents colours and will push you over the limit.
|
|
|
|
Lay the artwork out on a grid. A tile sheet is read as 8x8 cells row by row; a
|
|
sprite sheet is read as cells of whatever size you declare with `--cell`, also
|
|
row by row.
|
|
|
|
==== Baking Tiles
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 tools/assetbake/assetbake.py --type tile --target atarist \
|
|
font.png build/atarist/bin/DATA/font.tbk
|
|
----
|
|
|
|
Tile banks are *per-target*, because the 8x8 block layout is the platform's
|
|
native pixel format -- plane-major on Amiga and X68000, row-major-with-planes-
|
|
per-row on ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the
|
|
loader refuses a file baked for a different target rather than drawing garbage.
|
|
|
|
`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`,
|
|
`iigs` or `x68000`.
|
|
|
|
==== Baking Sprites
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 tools/assetbake/assetbake.py --type sprite --cell 2x3 \
|
|
player.png build/atarist/bin/DATA/player.spr
|
|
----
|
|
|
|
`--cell` gives the cel size *in tiles, not pixels*, so `2x3` means 16x24
|
|
pixels. The sheet is cut into cels row by row.
|
|
|
|
Sprite `.spr` banks are *cross-target*: the same file works on every platform,
|
|
because the sprite walker reads chunky 4bpp and converts at draw time. Bake
|
|
once, ship everywhere.
|
|
|
|
Both formats can embed the 16-entry `$0RGB` palette, and both loaders will hand
|
|
it back through their `outPalette` parameter so artwork carries its own colours
|
|
rather than depending on whatever the stage happens to have set.
|
|
|
|
==== Pre-Compiling Sprites
|
|
|
|
A `.spr` still needs compiling into native blitter code at run time. For a
|
|
large bank that is a visible startup pause. `spritebake` does it offline:
|
|
|
|
[source,console]
|
|
----
|
|
$ build/tools/spritebake-atarist player.spr player.spc
|
|
----
|
|
|
|
The result is a *per-platform* `.spc` holding pre-emitted native routines.
|
|
`jlSpriteBankLoadPrecompiled` copies them straight into the codegen arena and
|
|
skips compilation entirely. A cel too big for the arena silently loads
|
|
uncompiled rather than failing, so this is a safe drop-in replacement.
|
|
|
|
The `spritebake` binaries are built per target by `make/tools.mk`, because each
|
|
one links the emitter for its own CPU while running on the build host.
|
|
|
|
There is a third option if you would rather ship only the small cross-platform
|
|
`.spr`: compile once on first run and cache the result with
|
|
`jlSpriteBankSavePrecompiled`, then load the cache on later runs. That trades a
|
|
one-time startup cost for a much smaller distribution.
|
|
|
|
=== Music: the Portable Chip Tracker
|
|
|
|
This is the recommended music path. One authored song plays on every port
|
|
at register-write cost, with no CPU mixing -- which matters enormously on the
|
|
ST, where `.MOD` playback is only marginally viable.
|
|
|
|
Songs are plain text, one row per line:
|
|
|
|
[source]
|
|
----
|
|
; Four bars of Am - F - C - G.
|
|
tickms 110
|
|
loop
|
|
; voice0 voice1 voice2 noise
|
|
A-2:2 A-4:5 --- N16:5
|
|
--- C-5:5 E-5:3 off
|
|
A-3:3 E-4:5 off N2:7
|
|
--- A-4:5 C-5:3 off
|
|
----
|
|
|
|
The columns are three tone voices plus an optional fourth noise column.
|
|
|
|
[cols="1,4", options="header"]
|
|
|===
|
|
|Cell |Meaning
|
|
|
|
|`C-4:3`
|
|
|Note on: note name, octave, then `:` and an attenuation 0..14 where 0 is
|
|
loudest. Sharps use `#`, as in `C#4:3`. Range `C-0` to `B-7`. The `:atten`
|
|
part may be omitted and defaults to 0.
|
|
|
|
|`@440:3`
|
|
|Note on at an exact frequency in Hz, for chip-accurate transcription where
|
|
equal-tempered note names would round the pitch.
|
|
|
|
|`---`
|
|
|Hold: this voice is unchanged for this row.
|
|
|
|
|`off`
|
|
|Silence this voice.
|
|
|
|
|`N4:3`
|
|
|Noise on (fourth column only): pitch 0..31, where 0 is the brightest hiss
|
|
and 31 the lowest rumble, plus attenuation.
|
|
|===
|
|
|
|
Directives: `tickms N` sets milliseconds per row and is required before the
|
|
first row (later occurrences change tempo mid-song); `loop` marks the next row
|
|
as the loop-back target; `repeat N` ... `endrepeat` repeats a block, and may be
|
|
nested.
|
|
|
|
Bake it either as a binary blob or directly as a C array:
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 tools/songbake/songbake.py song.song song.jym
|
|
$ python3 tools/songbake/songbake.py -c gDemoSong song.song song.h
|
|
----
|
|
|
|
The C header form is usually what you want for a small song: it compiles into
|
|
the program and needs no file I/O at all.
|
|
|
|
[WARNING]
|
|
====
|
|
On the Atari ST the noise generator shares channel C's volume register with
|
|
tone voice 2. Author noise hits and voice-2 notes so they do not sound at the
|
|
same time; `songbake` warns when they overlap.
|
|
====
|
|
|
|
=== Music: Protracker Modules
|
|
|
|
For richer music, author a four-channel `.MOD` in any tracker (ProTracker,
|
|
MilkyTracker, OpenMPT). Then convert per target:
|
|
|
|
[source,console]
|
|
----
|
|
$ tools/joeymod/joeymod song.mod song.mod # DOS and ST: validated copy
|
|
$ tools/joeymod/joeymod song.mod song.amod # Amiga
|
|
$ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs
|
|
----
|
|
|
|
The output extension selects the conversion:
|
|
|
|
* *`.mod`* -- a passthrough copy with the signature validated. DOS and ST
|
|
detect song end inside libxmp-lite, so the file is used as-is.
|
|
* *`.amod`* -- the same, plus an `E8FF` stop marker injected at song end. The
|
|
Amiga replayer, PTPlayer, has no native song-end signal, so without this
|
|
marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore
|
|
`E8FF`, so an `.amod` is harmless if it reaches the wrong port.
|
|
* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the
|
|
NinjaTrackerPlus converter the installer's `ntp` component provides (part of
|
|
`toolchain:iigs`; it needs the PHP command-line interpreter, which the
|
|
installer's host packages include).
|
|
|
|
Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and
|
|
the sample data has to fit in memory alongside your game.
|
|
|
|
=== Sound Effects
|
|
|
|
Sound effects are *raw signed 8-bit PCM* with no header at all -- just sample
|
|
bytes. Convert from a WAV with any audio editor, or on the command line:
|
|
|
|
[source,console]
|
|
----
|
|
$ sox input.wav -r 11025 -c 1 -b 8 -e signed-integer output.sfx
|
|
----
|
|
|
|
Then play it, telling the library what rate it was recorded at:
|
|
|
|
[source,console]
|
|
----
|
|
jlAudioPlaySfx(4, sfxBytes, sfxLen, 11025);
|
|
----
|
|
|
|
Choose a rate that suits the machines: 8 to 11 kHz is plenty for game effects
|
|
and keeps the data small, which matters when the whole thing must fit on a
|
|
floppy.
|
|
|
|
Two things to know. Prefer *slot 4* if music is playing, because the IIgs maps
|
|
tracker tone voices onto slots 0 to 2 and the noise voice onto slot 3. And the
|
|
IIgs plays a sample value of -128 as -127, because its DOC reads the
|
|
corresponding byte as a wave-end marker -- one level out of 256, inaudible in
|
|
practice, but it explains a tiny waveform difference if you ever measure one.
|
|
|
|
For long sounds such as speech, use `jlAudioPlaySfxStream` with a fill callback
|
|
instead of holding the whole sample in memory.
|
|
|
|
=== Screens and Backgrounds
|
|
|
|
A whole 320x200 screen can be saved and reloaded in target-native form:
|
|
|
|
[source,c]
|
|
----
|
|
jlSurfaceSaveFile(surface, "title.sur");
|
|
jlSurfaceLoadFile(surface, "title.sur");
|
|
----
|
|
|
|
The file is 32000 bytes of pixels, 200 SCB bytes and 512 bytes of palette. It
|
|
is *not* portable between platforms -- no format conversion happens, so the
|
|
machine that wrote it must be the machine that reads it. It is best used as a
|
|
runtime cache (a generated title screen, say), not as a shipped asset. For
|
|
shipped artwork, bake a tile bank and compose the screen with
|
|
`jlTileBankLoadToSurface`.
|
|
|
|
=== Staging Assets into the Build
|
|
|
|
Baked files must end up in `DATA/` next to the executable. A program's
|
|
Makefile lists them in its `ID_DATA` and makes them with rules of its own,
|
|
written once for every target (<<_data_files>>). The simple copy case, from
|
|
JoeyLib's `make/examples.mk`:
|
|
|
|
[source,makefile]
|
|
----
|
|
AUDIO_DATA := $(addprefix $(call programDataDir,AUDIO)/,test.mod test.sfx)
|
|
|
|
$(call programDataDir,AUDIO)/%: $(REPO_DIR)/assets/%
|
|
@mkdir -p $(dir $@)
|
|
cp $< $@
|
|
----
|
|
|
|
And a bake rule looks like:
|
|
|
|
[source,makefile]
|
|
----
|
|
$(call programDataDir,MYGAME)/font.tbk: assets/font.png
|
|
@mkdir -p $(dir $@)
|
|
python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@
|
|
----
|
|
|
|
Tile banks and `.spc` files differ per platform, while `.spr`, `.jym` and
|
|
`.sfx` files are shared: `ASSET_PLATFORM` names the target a bake is for (DVX
|
|
takes the DOS bakes) and `SPRITEBAKE` the matching host `spritebake`.
|
|
|
|
`examples/tilebank` is a working reference: one rule in `make/examples.mk`
|
|
bakes the same PNG for each target into its `DATA/tilebank` (`TILEBANK_DATA`),
|
|
and the program loads that bank both ways and shows the loader refusing the
|
|
other targets' bakes. Its `scripts/verify-<port>-tilebank.sh` gates run it on
|
|
every port. Space Taxi, a game built on JoeyLib in a repository of its own,
|
|
has the full-sized one in its `program.mk`: levels extracted from a C64 image,
|
|
a cel bank generated by a host tool of its own and pre-compiled per target,
|
|
per-level banks marked optional, and speech baked with per-target flags.
|
|
|
|
=== Loading Assets at Run Time
|
|
|
|
[source,c]
|
|
----
|
|
// Tiles into a caller-owned array.
|
|
jlTileT tiles[256];
|
|
bool valid[256];
|
|
uint16_t palette[SURFACE_COLORS_PER_PALETTE];
|
|
uint16_t n = jlTileBankLoad("font.tbk", tiles, 256, valid, palette);
|
|
|
|
// Or stream a big bank straight onto a surface, with no caller buffer.
|
|
uint16_t m = jlTileBankLoadToSurface("font.tbk", fontSurface, palette);
|
|
|
|
// Sprites, pre-compiled.
|
|
jlSpriteT *cels[32];
|
|
uint16_t count = jlSpriteBankLoadPrecompiled("player.spc", cels, 32, palette);
|
|
|
|
// Apply the palette the artwork shipped with.
|
|
jlPaletteSet(stage, 0, palette);
|
|
----
|
|
|
|
Prefer `jlTileBankLoadToSurface` for anything large. A 1000-tile font sheet is
|
|
32000 bytes, which no IIgs program can hold in a local array or in bank-0 BSS,
|
|
and the streaming loader needs only a small internal scratch buffer.
|
|
|
|
=== The Logo
|
|
|
|
The JoeyLib logo lives in `assets/` as both the original 320x200 PNG and a
|
|
traced SVG. If the source art ever changes, regenerate the vector with:
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 -m venv /tmp/tracevenv
|
|
$ /tmp/tracevenv/bin/pip install potracer pillow numpy
|
|
$ /tmp/tracevenv/bin/python tools/traceLogo.py \
|
|
assets/joeylib_logo_-_320x200.png assets/joeylibLogo.svg --colors 5
|
|
----
|
|
|
|
Five colour clusters measured as the closest match to the original artwork;
|
|
more clusters start carving the antialiased edges into separate layers, which
|
|
renders as noise rather than detail.
|