diff --git a/cmd/view-set/main.go b/cmd/view-set/main.go index 610dd86..849dc4a 100644 --- a/cmd/view-set/main.go +++ b/cmd/view-set/main.go @@ -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) { diff --git a/doc/formats/img/altar_sprite_0.png b/doc/formats/img/altar_sprite_0.png new file mode 100644 index 0000000..087a795 Binary files /dev/null and b/doc/formats/img/altar_sprite_0.png differ diff --git a/doc/formats/obj.md b/doc/formats/obj.md index d82b068..c2a80ef 100644 --- a/doc/formats/obj.md +++ b/doc/formats/obj.md @@ -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. diff --git a/scripts/try-uncompress b/scripts/try-uncompress index 04264d3..af4008a 100755 --- a/scripts/try-uncompress +++ b/scripts/try-uncompress @@ -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