Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mysterious central value in sprite posts #1

Open
proydoha opened this issue Dec 15, 2024 · 0 comments
Open

Mysterious central value in sprite posts #1

proydoha opened this issue Dec 15, 2024 · 0 comments

Comments

@proydoha
Copy link

I've found your amazing article on Wolfenstein 3D game file specifications

It brought me a lot of joy and I've learned a thing or two in the process.
The only thing that was bothering me was this section:

Note: Posts are described by 3 values but the signification of the central one remains a mystery. All sources that I found (see that) concur that it is probably some kind of offset to a segment of the pixel pool but none fully understands it. It is possible to ignore this value when decoding sprites if all posts of all columns are decoded sequentially from the first (in that case values are read from the pixel pool in order from the start). However, when doing so there is no need to read the offsets to the first post of each column. The fact that these offsets are in the file shows that the developers wanted a way to decode the posts of a given column without decoding the previous ones, and they most likely use the mystery value to jump to the corresponding position in the pixel pool.

I think I've figured it out.
I think to get an offset into sprite chunk buffer you need to add: mysterious central value + post start (first value / 2)
This seemingly gives correct color index.

As I was retracing your steps I've made this crude sprite/texture extractor:

import sharp from 'sharp';

export class Wolf3DVswapExtractor {
    private readonly wolf3dGraphicsSize: number = 64;

    public async convertWallToPng(wallChunk: Buffer, filePath: string, palette: Uint32Array) {
        if (wallChunk.length === 0) {
            return;
        }

        const image = this.createEmptySharpImage(this.wolf3dGraphicsSize);
        const {data, info} = await image.raw().toBuffer({ resolveWithObject: true });

        for (let columnIndex: number = 0; columnIndex < this.wolf3dGraphicsSize; columnIndex++) {
            for (let rowIndex: number = 0; rowIndex < this.wolf3dGraphicsSize; rowIndex++) {
                const paletteIndex: number = wallChunk.readUInt8(columnIndex + rowIndex * this.wolf3dGraphicsSize);
                const color: RgbaColor = this.getColorFromPalette(paletteIndex, palette);
                this.writePixelToImageDataBuffer(rowIndex, columnIndex, data, info.width, color);
            }
        }

        await this.saveSharpImage(data, info, filePath);
    }

    public async convertSpriteToPng(spriteChunk: Buffer, filePath: string, palette: Uint32Array) {
        if (spriteChunk.length === 0) {
            return;
        }
        const image = this.createEmptySharpImage(this.wolf3dGraphicsSize);
        const {data, info} = await image.raw().toBuffer({ resolveWithObject: true });

        const spriteHeader: SpriteHeader = this.getSpriteHeader(spriteChunk);
        const posts: SpritePost[] = this.getSpritePosts(spriteChunk, spriteHeader);

        for (let i: number = 0; i < posts.length; i++) {
            const post: SpritePost = posts[i];
            let pixelPoolPosition: number = post.mysteryValue + post.start;
            for (let j: number = post.start; j < post.end; j++) {
                if (pixelPoolPosition >= spriteHeader.pixelPool.length) { break; }
                const colorIndex: number = spriteChunk.readUInt8(pixelPoolPosition);
                const color: RgbaColor = this.getColorFromPalette(colorIndex, palette);
                this.writePixelToImageDataBuffer(post.column, j, data, info.width, color);
                pixelPoolPosition++;
            }
        }

        await this.saveSharpImage(data, info, filePath);
    }

    private async saveSharpImage(data: Buffer, info: sharp.OutputInfo, filePath: string) {
        await sharp(data, { raw: info }).toFile(filePath);
    }

    private createEmptySharpImage(size: number): sharp.Sharp {
        const image = sharp({
            create: {
                width: size,
                height: size,
                channels: 4,
                background: { r: 0, g: 0, b: 0, alpha: 0 }
            }
        });
        return image;
    }

    private writePixelToImageDataBuffer(x: number, y: number, data: Buffer, imageWidth: number, color: RgbaColor) {
        const index: number = (imageWidth * y + x) * 4;
        data[index] = color.r;
        data[index + 1] = color.g;
        data[index + 2] = color.b;
        data[index + 3] = color.a;
    }

    private getColorFromPalette(colorIndex: number, palette: Uint32Array): RgbaColor {
        const colorBuffer: Buffer = Buffer.alloc(4);
        colorBuffer.writeUInt32LE(palette[colorIndex]);

        const alpha: number = colorBuffer.readUInt8(3);
        const blue: number = colorBuffer.readUInt8(2);
        const green: number = colorBuffer.readUInt8(1);
        const red: number = colorBuffer.readUInt8(0);
        return {
            r: red,
            g: green,
            b: blue,
            a: alpha
        };
    }

    private getSpriteHeader(buffer: Buffer): SpriteHeader {
        let bufferPosition: number = 0;

        const firstColumn: number = buffer.readUInt16LE(bufferPosition);
        bufferPosition += 2;
        const lastColumn: number = buffer.readUInt16LE(bufferPosition);
        bufferPosition += 2;

        const offsetCount: number = lastColumn - firstColumn + 1;
        const columnOffsets: number[] = [];

        for (let i: number = 0; i < offsetCount; i++) {
            const offset: number = buffer.readUInt16LE(bufferPosition);
            bufferPosition += 2;
            columnOffsets.push(offset);
        }

        const pixelPool: Buffer = buffer.subarray(bufferPosition, buffer.length - 1);

        return {
            firstColumn: firstColumn,
            lastColumn: lastColumn,
            columnOffsets: columnOffsets,
            pixelPool: pixelPool,
        };
    }

    private getSpritePosts(buffer: Buffer, spriteHeader: SpriteHeader): SpritePost[] {
        const posts: SpritePost[] = [];
        let bufferPosition: number = 4;
        for (let i: number = 0; i < spriteHeader.columnOffsets.length; i++) {
            bufferPosition = spriteHeader.columnOffsets[i];
            const columnIndex: number = spriteHeader.firstColumn + i;

            while (bufferPosition < buffer.length) {
                const doublePostEnd: number = buffer.readUInt16LE(bufferPosition);
                bufferPosition += 2;
                if (doublePostEnd === 0) {
                    break;
                }
                const mystery: number = buffer.readUInt16LE(bufferPosition);
                bufferPosition += 2;
                const doublePostStart: number = buffer.readUInt16LE(bufferPosition);
                bufferPosition += 2;

                const postStart: number = doublePostStart / 2;
                const postEnd: number = doublePostEnd / 2;

                posts.push({
                    column: columnIndex,
                    start: postStart,
                    end: postEnd,
                    mysteryValue: mystery
                });
            }
        }

        return posts;
    }
}

interface SpriteHeader {
    firstColumn: number;
    lastColumn: number;
    columnOffsets: number[];
    pixelPool: Buffer;
}

interface SpritePost {
    column: number;
    start: number;
    mysteryValue: number;
    end: number;
}

interface RgbaColor {
    r: number;
    g: number;
    b: number;
    a: number;
}

Thank you for your amazing article!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant