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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
// "image/color"
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
@@ -96,7 +97,7 @@ func (e *env) run() {
|
|||||||
"new state: numObj=%d object=%d (%s) numFrames=%d sprite=%d zoom=%.2f",
|
"new state: numObj=%d object=%d (%s) numFrames=%d sprite=%d zoom=%.2f",
|
||||||
state.env.set.Count(),
|
state.env.set.Count(),
|
||||||
state.objIdx,
|
state.objIdx,
|
||||||
state.env.set.Palette[state.objIdx],
|
state.env.set.Palette[state.objIdx], // FIXME: palette is a confusing name
|
||||||
state.curObject().NumSprites,
|
state.curObject().NumSprites,
|
||||||
state.spriteIdx,
|
state.spriteIdx,
|
||||||
state.zoom,
|
state.zoom,
|
||||||
@@ -115,22 +116,57 @@ func (s *state) runStep(pWin *pixelgl.Window) *state {
|
|||||||
return &newState
|
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) {
|
func (s *state) present(pWin *pixelgl.Window) {
|
||||||
// obj := s.curObject()
|
obj := s.curObject()
|
||||||
// sprite := obj.Sprites[s.spriteIdx]
|
sprite := obj.Sprites[s.spriteIdx]
|
||||||
|
pic := spriteToPic(sprite)
|
||||||
|
|
||||||
center := pWin.Bounds().Center()
|
center := pWin.Bounds().Center()
|
||||||
|
|
||||||
cam := pixel.IM
|
cam := pixel.IM
|
||||||
// cam = cam.ScaledXY(pixel.ZV, pixel.Vec{1.0, -1.0}) // invert the Y axis
|
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.Scaled(center, s.zoom) // apply current zoom factor
|
||||||
//cam = cam.Moved(center.Sub(s.camPos)) // Make it central
|
//cam = cam.Moved(center.Sub(s.camPos)) // Make it central
|
||||||
//cam = cam.Rotated(center, -0.785) // Apply isometric angle
|
//cam = cam.Rotated(center, -0.785) // Apply isometric angle
|
||||||
s.cam = cam
|
s.cam = cam
|
||||||
pWin.SetMatrix(s.cam)
|
pWin.SetMatrix(s.cam)
|
||||||
|
|
||||||
pWin.Clear(colornames.White)
|
pWin.Clear(colornames.Black)
|
||||||
// pixel.NewSprite(pic, pic.Bounds()).Draw(pWin, pixel.IM.Moved(center))
|
pixel.NewSprite(pic, pic.Bounds()).Draw(pWin, pixel.IM.Moved(center))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) handleKeys(pWin *pixelgl.Window) {
|
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
|
## 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-
|
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
|
pixel tile and assigns it a type of 13. There are six sprites, all of which are
|
||||||
identical. Data dump:
|
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
|
array of pixels. WH40K_TD.exe is a 256-colour application, so the pixel data may
|
||||||
account for 1 of the 3 bytes.
|
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
|
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
|
WH40K_TD.exe crashes trying to load a set referencing `pillar` in it unless it's
|
||||||
in the CENTER position. Interesting.
|
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 |
|
| Offset | Purpose |
|
||||||
| ------ | ------- |
|
| ------ | ------- |
|
||||||
| 0x0000 | ?
|
| 0x0000 | ?
|
||||||
@@ -248,8 +209,16 @@ d: 116 * 100 = 11600. 7368 - 24 = 7344. 5.1 bits / pixel
|
|||||||
| 0x0010 | Padding? |
|
| 0x0010 | Padding? |
|
||||||
| 0x0014 | Padding? |
|
| 0x0014 | Padding? |
|
||||||
|
|
||||||
We still don't know what the first 32 bits are all about. Perhaps they can help
|
We still don't know what the first 32 bits are all about.
|
||||||
to explain the differences in putative bpp.
|
|
||||||
|
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
|
0x002-0x003 changes in step with total number of pixels, but that doesn't seem
|
||||||
to account for the difference.
|
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
|
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`
|
`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
|
WH40K_TD.exe loops around "ReadInMissionFLCs", incl. address 0x0041dd10, where
|
||||||
it loads in all the .asc and .obj files in a set.
|
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)
|
nrows = (bytes.count / blocksize)
|
||||||
|
|
||||||
0.upto(nrows) do |i|
|
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 = bytes[(i*blocksize)...(i*blocksize+blocksize)]
|
||||||
|
|
||||||
block.concat([nil]*(blocksize-block.size)) if block.size < blocksize
|
block.concat([nil]*(blocksize-block.size)) if block.size < blocksize
|
||||||
|
|
||||||
out = [
|
out = [
|
||||||
"0x#{hex(i*blocksize, 4)}",
|
# "0x#{hex(i*blocksize, 4)}",
|
||||||
block.map { |b| hex(b, 2) }, # hex
|
block.map { |b| hex(b, 2) }, # hex
|
||||||
" | " + block.map { |b| text(b) }.join("") + " |", # ascii
|
# " | " + block.map { |b| text(b) }.join("") + " |", # ascii
|
||||||
block.map { |b| ascii(b) } ,# decimal bytes
|
# block.map { |b| ascii(b) } ,# decimal bytes
|
||||||
"",# decimal 2-bytes
|
# "",# decimal 2-bytes
|
||||||
# decimal 4-bytes
|
# decimal 4-bytes
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -266,7 +266,6 @@ def decompress(filename)
|
|||||||
loop do
|
loop do
|
||||||
break if data.empty?
|
break if data.empty?
|
||||||
|
|
||||||
type = data.shift(1)[0]
|
|
||||||
right = data.index(0)
|
right = data.index(0)
|
||||||
|
|
||||||
|
|
||||||
@@ -278,12 +277,18 @@ def decompress(filename)
|
|||||||
rec = data.shift(right)
|
rec = data.shift(right)
|
||||||
_ = data.shift(1) # drop the record separator
|
_ = data.shift(1) # drop the record separator
|
||||||
|
|
||||||
decompressed << [ type, rec[0] ]
|
decompressed << rec
|
||||||
end
|
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}"
|
puts "WARNING: #{data.size} bytes left over" if data.size > 0
|
||||||
pp decompressed
|
|
||||||
# display(decompressed.map(&:chr).join(""))
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user