Determine the RLE format for .obj file sprite pixeldata
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/loader
|
/loader
|
||||||
/orig
|
/orig
|
||||||
|
/palette-idx
|
||||||
/view-obj
|
/view-obj
|
||||||
/view-map
|
/view-map
|
||||||
/view-minimap
|
/view-minimap
|
||||||
|
5
Makefile
5
Makefile
@@ -1,10 +1,13 @@
|
|||||||
srcfiles = $(shell find . -iname *.go)
|
srcfiles = $(shell find . -iname *.go)
|
||||||
|
|
||||||
all: loader view-obj view-map view-minimap view-set
|
all: loader palette-idx view-obj view-map view-minimap view-set
|
||||||
|
|
||||||
loader: $(srcfiles)
|
loader: $(srcfiles)
|
||||||
go build -o loader ur.gs/ordoor/cmd/loader
|
go build -o loader ur.gs/ordoor/cmd/loader
|
||||||
|
|
||||||
|
palette-idx: $(srcfiles)
|
||||||
|
go build -o palette-idx ur.gs/ordoor/cmd/palette-idx
|
||||||
|
|
||||||
view-obj: $(srcfiles)
|
view-obj: $(srcfiles)
|
||||||
go build -o view-obj ur.gs/ordoor/cmd/view-obj
|
go build -o view-obj ur.gs/ordoor/cmd/view-obj
|
||||||
|
|
||||||
|
11
README.md
11
README.md
@@ -42,9 +42,10 @@ The `view-map` binary attempts to render a map, and is the current focus of
|
|||||||
effort. Once I can render a whole map, including pre-placed characters (cultist
|
effort. Once I can render a whole map, including pre-placed characters (cultist
|
||||||
scum), things can start to get more interesting.
|
scum), things can start to get more interesting.
|
||||||
|
|
||||||
Current status: map tiles are rendered at correct offsets. Tiles (four per
|
Current status: map tiles are rendered at correct offsets. Static objects (four
|
||||||
coordinate: floor, centre, left, and right) are rendered, but the pixels for
|
per map coordinate: floor, centre, left, and right) are rendered mostly fine,
|
||||||
those tiles are not 100% accurate.
|
although objects at each Z level don't *quite* stack correctly on top of each
|
||||||
|
other yet.
|
||||||
|
|
||||||
Characters and animations aren't touched at all yet. Rendering performance is
|
Characters and animations aren't touched at all yet. Rendering performance is
|
||||||
atrocious. No gameplay, no sound, no campaign logic. Interaction with the play
|
atrocious. No gameplay, no sound, no campaign logic. Interaction with the play
|
||||||
@@ -59,6 +60,10 @@ $ make view-map
|
|||||||
$ ./view-map -map orig/Maps/Chapter01.MAP -txt orig/Maps/Chapter01.TXT
|
$ ./view-map -map orig/Maps/Chapter01.MAP -txt orig/Maps/Chapter01.TXT
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Looks like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the
|
Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the
|
||||||
`1` - `7` keys to change Z level.
|
`1` - `7` keys to change Z level.
|
||||||
|
|
||||||
|
26
cmd/palette-idx/main.go
Normal file
26
cmd/palette-idx/main.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"ur.gs/ordoor/internal/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "index", "i":
|
||||||
|
idx, err := strconv.ParseInt(os.Args[2], 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Usage: palette-idx i <0-255>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Palette[%v]: %#v\n", idx, data.ColorPalette[idx])
|
||||||
|
case "color", "colour", "c":
|
||||||
|
fmt.Println("TODO!")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
doc/formats/img/altar.obj.me-2018-09-07.png
Normal file
BIN
doc/formats/img/altar.obj.me-2018-09-07.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
doc/formats/img/altar.obj.original.png
Normal file
BIN
doc/formats/img/altar.obj.original.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
doc/formats/img/chapter01_rendered_2018-09-08.png
Normal file
BIN
doc/formats/img/chapter01_rendered_2018-09-08.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
@@ -85,8 +85,8 @@ The `type` field **may** tell us what format each sprite is in.
|
|||||||
`.obj` files represent visual data. They contain a number of sprites, which are
|
`.obj` files represent visual data. They contain a number of sprites, which are
|
||||||
assigned attributes in `.asn` files and referenced from `.map` files.
|
assigned attributes in `.asn` files and referenced from `.map` files.
|
||||||
|
|
||||||
The container format is worked out, but the per-sprite data is still unknown, so
|
The container format is worked out, but the per-sprite data is still WIP, so I'm
|
||||||
I'm documenting the former here while still investigating the latter.
|
documenting the former here while still investigating the latter.
|
||||||
|
|
||||||
The file begins with a header, with all values 32-bit little-endians:
|
The file begins with a header, with all values 32-bit little-endians:
|
||||||
|
|
||||||
@@ -357,6 +357,149 @@ following pixeldata, and then a trailing pair of bytes with the same value.
|
|||||||
|
|
||||||
For these tiles, the central (widest) row has 0x7f instead of 0x80.
|
For these tiles, the central (widest) row has 0x7f instead of 0x80.
|
||||||
|
|
||||||
|
|
||||||
|
## Comparative rendering
|
||||||
|
|
||||||
|
Here, I'm focusing on the `altar.obj` file. It has just two sprites and is a
|
||||||
|
`CENTER` object. Here's how it looks when rendered by `WH40K_TD.exe` (there's
|
||||||
|
a cloud of uncertainty around the skulls at present):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
And here's the rendering of the first sprite, done by my code as of 2018-09-07:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Initial impressions:
|
||||||
|
|
||||||
|
* The original clearly has some transparent pixels
|
||||||
|
* Skulls are scrambled, more so to the right
|
||||||
|
* Starting X-axis along the top half is well out
|
||||||
|
* The tabletop has become basically invisible
|
||||||
|
|
||||||
|
I think some sort of RLE *must* be in place for repeated pixels in a line.
|
||||||
|
|
||||||
|
Digging into the pixeldata, we have 3 distinct stages in the first 16 records
|
||||||
|
(which are null-terminated):
|
||||||
|
|
||||||
|
1. Top of skull
|
||||||
|
1. Right hand side is marked by a vertical antenna-like thing
|
||||||
|
1. The stepped altar resumes on the right.
|
||||||
|
|
||||||
|
Header:
|
||||||
|
|
||||||
|
```
|
||||||
|
------------------
|
||||||
|
0 1 2 3
|
||||||
|
------------------
|
||||||
|
0x0000 d6 00 16 01
|
||||||
|
0x0004 74 00 67 00 (x = 116, y = 103)
|
||||||
|
0x0008 00 00 00 00
|
||||||
|
0x000c 11 1d 00 00
|
||||||
|
0x0010 00 00 00 00
|
||||||
|
0x0014 00 00 00 00
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Here's the first row of pixeldata:
|
||||||
|
|
||||||
|
```
|
||||||
|
80 3d 82 65 1d 80 35 00 (0)
|
||||||
|
```
|
||||||
|
|
||||||
|
There are just two visible pixels in this row, and they are specified correctly
|
||||||
|
by the middle two bytes.
|
||||||
|
|
||||||
|
The 3 on either side are framing data, for certain. It's not clear exactly what
|
||||||
|
they specify though.
|
||||||
|
|
||||||
|
Onto the next row:
|
||||||
|
|
||||||
|
```
|
||||||
|
80 38 87 97 19 17 18 17 18 16 80 35 00 (1)
|
||||||
|
```
|
||||||
|
|
||||||
|
These pixels end in the same position on the right, but start 5 pixels earlier.
|
||||||
|
So there are 7 pixels total, specified by the middle bytes again.
|
||||||
|
|
||||||
|
The RHS bytes are identical. The LHS bytes are 5 lower and higher, respectively.
|
||||||
|
|
||||||
|
Hmm. More rows, not analyzed in depth yet:
|
||||||
|
|
||||||
|
```
|
||||||
|
80 37 82 17 19 03 17 04 16 80 34 00 ( 2)
|
||||||
|
80 36 84 18 16 15 15 04 16 83 14 14 16 80 33 00 ( 3)
|
||||||
|
80 35 83 17 17 16 03 17 81 18 03 17 83 a8 16 17 80 32 00 ( 4)
|
||||||
|
|
||||||
|
80 35 8e 18 19 aa 18 17 17 18 18 17 a8 a8 17 18 18 80 02 81 ad 80 2e 00 ( 5) - right vertical bar starts
|
||||||
|
80 34 8f 18 ab 19 18 aa 19 19 ab ab 19 aa aa 17 17 19 80 02 81 ad 80 2e 00 ( 6)
|
||||||
|
80 34 8c 19 ab 19 19 1a ad ad 1b ad ad 1a ab 03 17 81 19 80 01 81 ad 80 2e 00 ( 7)
|
||||||
|
80 34 90 ac 19 18 18 ab 1b ae 1c ae 1c ad ab 17 16 aa 1b 80 01 81 ae 80 2e 00 ( 8)
|
||||||
|
80 33 91 1a 1c ac 18 19 1a 1b 1c ae 1c 1c ad 1a 19 17 19 ae 80 01 81 ae 80 2e 00 ( 9)
|
||||||
|
80 33 91 1a 1c ad 1a ad 1c ae 1c ae 1c 1c ad 1c ad 1a ab ad 80 01 81 ae 80 2e 00 (10)
|
||||||
|
|
||||||
|
80 31 81 8b 80 01 91 19 1c 1a ad ae 1d 1c af 1d 1c af 1c ae ae 1b ab ad 80 01 81 af 80 2e 00 (11) - left vertical bar starts
|
||||||
|
80 31 81 ab 80 02 86 1c ad 1b 1c 1c af 04 1d 86 1c ae 1b ad 1a ae 80 01 81 af 80 2e 00 (12)
|
||||||
|
80 31 81 ad 80 01 93 1f 1c 1c 1b 1c 1b ae 1d 1c 1d ae 1c ad ad 1b ae 1c 1f ad 80 2e 00 (13)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now for something more interesting. At line 14, an antenna starts to sprout on
|
||||||
|
the far left. It is one pixel wide, followed by 19 transparent pixels, followed
|
||||||
|
by another antenna and the skull.
|
||||||
|
|
||||||
|
```
|
||||||
|
1 2 3 4
|
||||||
|
80 1d 81 ad 80 13 85 ac 1f 1f af 1d 08 1c 89 1b 1b 1c ae 1c 1a 1f af 1f 80 2d 00 (14) - right vertical bar is no longer the last thing
|
||||||
|
80 1d 81 ac 80 11 83 1f 1f ac 03 1f 81 af 09 1d 87 1c 1d ae 1d 1f 1f ae 03 1f 80 2b 00 (15)
|
||||||
|
80 1d 81 ac 80 0f 04 1f 81 ab 03 1f 88 1c 1d 17 15 15 1d 1e 1c 05 1d 03 1f 81 af 05 1f 80 29 00
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Looking at `y=14`, the first couplet *must* define where we start, and
|
||||||
|
that all bytes before then are transparent. Looks like `0x80 <count>`
|
||||||
|
|
||||||
|
The second couplet writes 1 byte in a defined colour. Palette indices 0xad and
|
||||||
|
0xac match the pixels rendered by `WH40K_TD.exe`.
|
||||||
|
|
||||||
|
The third couplet defines the transparent section between the far left antenna
|
||||||
|
and the left antenna or table. Like the first couplet, it is `0x80 <count>`.
|
||||||
|
|
||||||
|
At 4, the couplet idea starts to break down, but this is definitely starting to
|
||||||
|
look RLE-ish.
|
||||||
|
|
||||||
|
Let's go back to line 2 with the knowledge above. We have 9 literal bytes to
|
||||||
|
display. Decoded, they should equal:
|
||||||
|
|
||||||
|
```
|
||||||
|
17 19 17 17 17 16 16 16 16
|
||||||
|
```
|
||||||
|
|
||||||
|
(these are the indexes for the colours each pixel is, as rendered by
|
||||||
|
`WH40K_TD.EXE`).
|
||||||
|
|
||||||
|
The encoded bytes:
|
||||||
|
|
||||||
|
```
|
||||||
|
80 37 82 17 19 03 17 04 16 80 34 00 ( 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
We can get to the output by applying these rules to each byte of encoded data:
|
||||||
|
|
||||||
|
* `< 0x80`: repeat the next byte that many times
|
||||||
|
* `= 0x80`: skip `<count>` bytes
|
||||||
|
* `> 0x80`: take literal - 0x80 bytes
|
||||||
|
|
||||||
|
Doing so, the altar displays, but more importantly, so does all of
|
||||||
|
`Chapter01.MAP`!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The annoying "static" is gone, and the only rendering issue visible is that the
|
||||||
|
objects from different Z levels don't stack correctly. Hooray for progress!
|
||||||
|
|
||||||
|
I haven't tried any of the other objects in different chapters yet. Perhaps some
|
||||||
|
of them will fail to render?
|
||||||
|
|
||||||
## Debugger
|
## Debugger
|
||||||
|
|
||||||
|
|
||||||
|
@@ -52,60 +52,52 @@ var transparent = color.RGBA{0, 0, 0, 0}
|
|||||||
func spriteToPic(name string, idx int, sprite *data.Sprite) *pixel.PictureData {
|
func spriteToPic(name string, idx int, sprite *data.Sprite) *pixel.PictureData {
|
||||||
pic := pixel.MakePictureData(pixel.R(float64(0), float64(0), float64(sprite.Width), float64(sprite.Height)))
|
pic := pixel.MakePictureData(pixel.R(float64(0), float64(0), float64(sprite.Width), float64(sprite.Height)))
|
||||||
|
|
||||||
//log.Printf("%v %v: width=%v height=%v", name, idx, sprite.Width, sprite.Height)
|
log.Printf("%v %v: width=%v height=%v", name, idx, sprite.Width, sprite.Height)
|
||||||
|
|
||||||
for y := 0; y < int(sprite.Height); y++ {
|
for y := 0; y < int(sprite.Height); y++ {
|
||||||
|
encoded := sprite.Rows[y]
|
||||||
|
decoded := make([]byte, 0, int(sprite.Width))
|
||||||
|
|
||||||
// Start with all bytes transparent
|
// Start with all bytes transparent
|
||||||
for x := 0; x < int(sprite.Width); x++ {
|
for x := 0; x < int(sprite.Width); x++ {
|
||||||
pic.Pix[pic.Index(pixel.V(float64(x), float64(y)))] = transparent
|
pic.Pix[pic.Index(pixel.V(float64(x), float64(y)))] = transparent
|
||||||
}
|
}
|
||||||
|
|
||||||
row := sprite.Rows[y]
|
for i := 0; i < len(encoded); i++ {
|
||||||
//log.Printf("%#v", row)
|
b := encoded[i]
|
||||||
pixels := row[0 : len(row)-1] // Strip off the record separator (0x00)
|
|
||||||
|
|
||||||
// Not really clear on what this does yet. Aligned with sprite width in
|
// This appears to be a kind of RLE
|
||||||
// many cases but can also vary above and below that value.
|
if b == 0 {
|
||||||
u0 := int(pixels[0])
|
continue // finished
|
||||||
pixels = pixels[1:len(pixels)]
|
} else if b < 0x80 {
|
||||||
|
// repeat the next byte this many times
|
||||||
|
for j := 0; j < int(b); j++ {
|
||||||
|
decoded = append(decoded, encoded[i+1])
|
||||||
|
}
|
||||||
|
|
||||||
// In some cases, the column data is indented relative to the start of
|
i++ // skip the repeat byte
|
||||||
// the row. Certainly true when u0 == 0x80, perhaps in other cases
|
} else if b == 0x80 {
|
||||||
// too.
|
// transparent value, skip forward *x+1 rows
|
||||||
//
|
skip := int(encoded[i+1])
|
||||||
// Definitely not the case when u0 == 0x01 - there aren't enough bytes
|
for i := 0; i < skip; i++ {
|
||||||
// in that case for it to be anything but pixeldata
|
decoded = append(decoded, byte(0x00))
|
||||||
xOffset := 0
|
}
|
||||||
|
|
||||||
// Do nothing if we're out of pixels
|
i++ // skip the count byte
|
||||||
if u0 == 0x80 {
|
} else {
|
||||||
//log.Printf("Handling 0x80: %#v", pixels)
|
// take the next b-0x80 bytes literally
|
||||||
xOffset = int(pixels[0])
|
literals := int(b) - 0x80
|
||||||
pixels = pixels[1:len(pixels)]
|
for j := i + 1; j <= i+literals; j++ {
|
||||||
|
decoded = append(decoded, encoded[j])
|
||||||
|
}
|
||||||
|
|
||||||
// Sometimes, pixels is now empty. e.g. l_ivy02 sprite 6
|
i = i + literals
|
||||||
if len(pixels) > 3 {
|
|
||||||
|
|
||||||
// For tiles, this has an inverse relationship with u0. Seems to add
|
|
||||||
// up to 0x42 in all cases, which matches byte 3 of the header?
|
|
||||||
//_ = int(pixels[0])
|
|
||||||
|
|
||||||
pixels = pixels[1:len(pixels)]
|
|
||||||
|
|
||||||
// On tiles, this removes some junk around the edge, but doesn't
|
|
||||||
// seem to be reasonable in-general?
|
|
||||||
pixels = pixels[0 : len(pixels)-2]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Printf(
|
// Update the picture
|
||||||
// "%v %d: len(row)=%v, len(pixels)=%v sprWidth=%v u0=%v xOffset=%v",
|
for x, b := range decoded {
|
||||||
// name, idx, len(row), len(pixels), sprite.Width, u0, xOffset,
|
if err := setPaletteColor(pic, x, y, b); err != nil {
|
||||||
//)
|
|
||||||
|
|
||||||
for x, b := range pixels {
|
|
||||||
vec := pixel.V(float64(xOffset+x), float64(y))
|
|
||||||
if err := setPaletteColor(pic, vec, b); err != nil {
|
|
||||||
log.Printf("%s %d: %d,%d: %v", name, idx, x, y, err)
|
log.Printf("%s %d: %d,%d: %v", name, idx, x, y, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,8 +106,9 @@ func spriteToPic(name string, idx int, sprite *data.Sprite) *pixel.PictureData {
|
|||||||
return pic
|
return pic
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPaletteColor(pic *pixel.PictureData, point pixel.Vec, colorIdx byte) error {
|
func setPaletteColor(pic *pixel.PictureData, x int, y int, colorIdx byte) error {
|
||||||
idx := pic.Index(point)
|
vec := pixel.V(float64(x), float64(y))
|
||||||
|
idx := pic.Index(vec)
|
||||||
|
|
||||||
if idx > len(pic.Pix)-1 {
|
if idx > len(pic.Pix)-1 {
|
||||||
return fmt.Errorf("Got index %v which exceeds bounds", idx)
|
return fmt.Errorf("Got index %v which exceeds bounds", idx)
|
||||||
|
Reference in New Issue
Block a user