Sprites in .obj files are composed of Y null-separated records with a probable type field
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
// "image/color"
|
||||
"image/color"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
@@ -96,7 +97,7 @@ func (e *env) run() {
|
||||
"new state: numObj=%d object=%d (%s) numFrames=%d sprite=%d zoom=%.2f",
|
||||
state.env.set.Count(),
|
||||
state.objIdx,
|
||||
state.env.set.Palette[state.objIdx],
|
||||
state.env.set.Palette[state.objIdx], // FIXME: palette is a confusing name
|
||||
state.curObject().NumSprites,
|
||||
state.spriteIdx,
|
||||
state.zoom,
|
||||
@@ -115,22 +116,57 @@ func (s *state) runStep(pWin *pixelgl.Window) *state {
|
||||
return &newState
|
||||
}
|
||||
|
||||
// WIP. Try to convert the pixeldata into a picture.
|
||||
func spriteToPic(sprite *data.Sprite) *pixel.PictureData {
|
||||
pic := pixel.MakePictureData(pixel.R(float64(0), float64(0), float64(sprite.Width), float64(sprite.Height)))
|
||||
|
||||
buf := bytes.NewBuffer(sprite.PixelData)
|
||||
|
||||
// The pixeldata seems to be formed of Y null-terminated records, with
|
||||
// varying numbers of bytes in each row. Probably [type, *data] but ignore
|
||||
// type for now.
|
||||
//
|
||||
// Theory: perhaps the data in each X is centered around the origin?
|
||||
for y := 0; y < int(sprite.Height); y++ {
|
||||
rowData, err := buf.ReadBytes(0)
|
||||
if err != nil {
|
||||
log.Printf("Error at y=%d: %v", y, err)
|
||||
continue
|
||||
}
|
||||
|
||||
leftPad := (int(sprite.Width) - len(rowData)) / 2
|
||||
|
||||
for x, b := range rowData {
|
||||
idx := pic.Index(pixel.V(float64(leftPad+x), float64(y)))
|
||||
pic.Pix[idx] = color.RGBA{
|
||||
R: b,
|
||||
G: b,
|
||||
B: b,
|
||||
A: 255,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pic
|
||||
}
|
||||
|
||||
func (s *state) present(pWin *pixelgl.Window) {
|
||||
// obj := s.curObject()
|
||||
// sprite := obj.Sprites[s.spriteIdx]
|
||||
obj := s.curObject()
|
||||
sprite := obj.Sprites[s.spriteIdx]
|
||||
pic := spriteToPic(sprite)
|
||||
|
||||
center := pWin.Bounds().Center()
|
||||
|
||||
cam := pixel.IM
|
||||
// cam = cam.ScaledXY(pixel.ZV, pixel.Vec{1.0, -1.0}) // invert the Y axis
|
||||
cam = cam.Scaled(center, s.zoom) // apply current zoom factor
|
||||
cam = cam.ScaledXY(center, pixel.Vec{1.0, -1.0}) // invert the Y axis
|
||||
cam = cam.Scaled(center, s.zoom) // apply current zoom factor
|
||||
//cam = cam.Moved(center.Sub(s.camPos)) // Make it central
|
||||
//cam = cam.Rotated(center, -0.785) // Apply isometric angle
|
||||
s.cam = cam
|
||||
pWin.SetMatrix(s.cam)
|
||||
|
||||
pWin.Clear(colornames.White)
|
||||
// pixel.NewSprite(pic, pic.Bounds()).Draw(pWin, pixel.IM.Moved(center))
|
||||
pWin.Clear(colornames.Black)
|
||||
pixel.NewSprite(pic, pic.Bounds()).Draw(pWin, pixel.IM.Moved(center))
|
||||
}
|
||||
|
||||
func (s *state) handleKeys(pWin *pixelgl.Window) {
|
||||
|
BIN
doc/formats/img/altar_sprite_0.png
Normal file
BIN
doc/formats/img/altar_sprite_0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
@@ -115,11 +115,6 @@ For sanity checks, we can ensure that:
|
||||
|
||||
## Sprite structure
|
||||
|
||||
If the `type` field of an `.asn` field *does* tell us how to interpret sprite
|
||||
data, I'll need to split this up per type. For now, I'm investigating a small
|
||||
number of files in depth, and comparing across files in a shallow manner, so all
|
||||
I need to do is manually select sets with the same type in the latter case.
|
||||
|
||||
First, the `blank.obj` file. `blank.asn` helpfully tells us that it's a single-
|
||||
pixel tile and assigns it a type of 13. There are six sprites, all of which are
|
||||
identical. Data dump:
|
||||
@@ -148,14 +143,6 @@ The colour itself doesn't show up in the data directly, so it's not a simple RGB
|
||||
array of pixels. WH40K_TD.exe is a 256-colour application, so the pixel data may
|
||||
account for 1 of the 3 bytes.
|
||||
|
||||
All sprites seem to end with 0x00 - like a GIF image descriptor, this may say
|
||||
"end of data".
|
||||
|
||||
So what's the 1? Simple RLE, like FLIC type 15 BYTE_RUN? - "Repeat 253 one time"?
|
||||
|
||||
There are 45 `TYPE 13` .obj files in the game. Comparing the above with the
|
||||
start of the second sprite in `pillar.obj`:
|
||||
|
||||
```
|
||||
----------------------------------------------
|
||||
0 1 2 3 4 5 6 7 01234567
|
||||
@@ -213,32 +200,6 @@ close correspondence in all cases.
|
||||
WH40K_TD.exe crashes trying to load a set referencing `pillar` in it unless it's
|
||||
in the CENTER position. Interesting.
|
||||
|
||||
Not all pixeldatas are evenly divisible by 3. blank.obj seems to be the special
|
||||
case.
|
||||
|
||||
The volume represented by a cell is a little odd. We see three faces of a fake
|
||||
3D volume of size 64x64x32(ish). This is presented in an isomorphic fashion, so
|
||||
the height is 32px at the leftmost and rightmost extents and 96 in the centre.
|
||||
Overall width is 128px, and the minimum rectangle covering the whole space would
|
||||
be 128x96, or 12288 bytes at 8bpp.
|
||||
|
||||
Or it could be represented as the three planes separately. So for an altar, we'd
|
||||
have 64x64 pixels, plus 32x64 pixels, plus 32x64 pixels (minus a few around the
|
||||
edges, perhaps) - 4096 + 2048 + 2048 = 8192. The sprite is only slightly smaller
|
||||
than that.
|
||||
|
||||
Or perhaps we draw a normal X/Y rectangle which is then skewed appropriately.
|
||||
That seems like it would be odd for pixeldata though?
|
||||
|
||||
Bytes per pixel (intuited from hypothetical x,y) for the four frames so far:
|
||||
|
||||
```
|
||||
a: 1 * 1 = 1. 27 - 24 = 3. 24 bits / pixel
|
||||
b: 46 * 72 = 3312. 3340 - 24 = 3316. 8 bits / pixel with 4 bytes left over.
|
||||
c: 116 * 103 = 11948. 7465 - 24 = 7439. 4.9 bits / pixel
|
||||
d: 116 * 100 = 11600. 7368 - 24 = 7344. 5.1 bits / pixel
|
||||
```
|
||||
|
||||
| Offset | Purpose |
|
||||
| ------ | ------- |
|
||||
| 0x0000 | ?
|
||||
@@ -248,8 +209,16 @@ d: 116 * 100 = 11600. 7368 - 24 = 7344. 5.1 bits / pixel
|
||||
| 0x0010 | Padding? |
|
||||
| 0x0014 | Padding? |
|
||||
|
||||
We still don't know what the first 32 bits are all about. Perhaps they can help
|
||||
to explain the differences in putative bpp.
|
||||
We still don't know what the first 32 bits are all about.
|
||||
|
||||
The volume represented by a cell is a little odd. We see three faces of a fake
|
||||
3D volume of size 64x64x32(ish). This is presented in an isomorphic fashion, so
|
||||
the height is 32px at the leftmost and rightmost extents and 96 in the centre.
|
||||
Overall width is 128px, and the minimum rectangle covering the whole space would
|
||||
be 128x96, or 12288 bytes at 8bpp.
|
||||
|
||||
It seems pixels can be larger than a cell - TZEENTCH.OBJ is almost 2 cells high,
|
||||
for instance.
|
||||
|
||||
0x002-0x003 changes in step with total number of pixels, but that doesn't seem
|
||||
to account for the difference.
|
||||
@@ -269,6 +238,25 @@ first byte of data for these 1x1 tiles.
|
||||
Sprites with `X= 128 Y=63` *almost always* seem to have a u0..u3 of
|
||||
`d1 00 42 01` with a few exceptions, e.g. `treemac{1,2}.obj`
|
||||
|
||||
Investigating the sprite data a little more, it seems that we tend to have Y
|
||||
null-separated records: 1 record for sprites with a height of 1, 100 for those
|
||||
with a height of 100, etc.
|
||||
|
||||
The first byte of each record seems likely to specify format - it's mostly
|
||||
invariant (always 1 for the 1x1 examples, almost always 0x80 for the altar
|
||||
sprites and the majority of other .obj files). Some other values appear too, but
|
||||
not a wide distribution.
|
||||
|
||||
Number of bytes per row varies. `altar.obj` is diamond-shaped, and the widths
|
||||
are also diamond-shaped, so perhaps 0x80 means "center the data around the X
|
||||
origin and assume anything unspecified is transparent"?
|
||||
|
||||
Result, with my rendering according to those rules on the left and WH40K_TD.exe
|
||||
on the right:
|
||||
|
||||

|
||||
|
||||
Hurrah!
|
||||
|
||||
WH40K_TD.exe loops around "ReadInMissionFLCs", incl. address 0x0041dd10, where
|
||||
it loads in all the .asc and .obj files in a set.
|
||||
|
@@ -202,17 +202,17 @@ def display(data, blocksize=8, skip=0, header: false)
|
||||
nrows = (bytes.count / blocksize)
|
||||
|
||||
0.upto(nrows) do |i|
|
||||
header!(blocksize) if i%16==0 || i == skip
|
||||
header!(blocksize) if header && (i%16==0 || i == skip)
|
||||
block = bytes[(i*blocksize)...(i*blocksize+blocksize)]
|
||||
|
||||
block.concat([nil]*(blocksize-block.size)) if block.size < blocksize
|
||||
|
||||
out = [
|
||||
"0x#{hex(i*blocksize, 4)}",
|
||||
# "0x#{hex(i*blocksize, 4)}",
|
||||
block.map { |b| hex(b, 2) }, # hex
|
||||
" | " + block.map { |b| text(b) }.join("") + " |", # ascii
|
||||
block.map { |b| ascii(b) } ,# decimal bytes
|
||||
"",# decimal 2-bytes
|
||||
# " | " + block.map { |b| text(b) }.join("") + " |", # ascii
|
||||
# block.map { |b| ascii(b) } ,# decimal bytes
|
||||
# "",# decimal 2-bytes
|
||||
# decimal 4-bytes
|
||||
]
|
||||
|
||||
@@ -266,7 +266,6 @@ def decompress(filename)
|
||||
loop do
|
||||
break if data.empty?
|
||||
|
||||
type = data.shift(1)[0]
|
||||
right = data.index(0)
|
||||
|
||||
|
||||
@@ -278,12 +277,18 @@ def decompress(filename)
|
||||
rec = data.shift(right)
|
||||
_ = data.shift(1) # drop the record separator
|
||||
|
||||
decompressed << [ type, rec[0] ]
|
||||
decompressed << rec
|
||||
end
|
||||
puts ": #{decompressed.size} records. Sprite pixels: #{hdr.width*hdr.height}"
|
||||
|
||||
puts ": #{data.size} bytes remaining. Decompressed: #{decompressed.size} bytes. Sprite pixels: #{hdr.width*hdr.height}"
|
||||
pp decompressed
|
||||
# display(decompressed.map(&:chr).join(""))
|
||||
puts "WARNING: #{data.size} bytes left over" if data.size > 0
|
||||
|
||||
header!
|
||||
decompressed.each_with_index do |line, y|
|
||||
x = line.size
|
||||
padding = (sprite.header.width - x) / 2
|
||||
display((["\x00"]*padding + line.map(&:chr)).join(""), line.size+padding.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
Reference in New Issue
Block a user