Sprites in .obj files are composed of Y null-separated records with a probable type field

This commit is contained in:
2018-03-21 22:49:46 +00:00
parent e3a2096b00
commit 5ee1ceb532
4 changed files with 88 additions and 59 deletions

View File

@@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -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:
![Altar sprite 0 rendering attempt](img/altar_sprite_0.png)
Hurrah!
WH40K_TD.exe loops around "ReadInMissionFLCs", incl. address 0x0041dd10, where
it loads in all the .asc and .obj files in a set.

View File

@@ -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