Search code examples
csdlaffinetransformperspectivecameraprojection-matrix

The cause of uncontrolled camera movement in a mode 7 affine transformation?


I am trying to implement a mode 7 effect with C and SDL2. Eventually, I hope to be able to do cool effects, like change the height and field of view. But for now, I just want to get something simple working. This is what I have so far:

enter image description here

My problem is that that upon turning, I seem to be heading in the same direction, even when I try to turn a different way. It's hard to get a picture of this, but upon running my code, it should become clear.

I am very confused why this is the case. My code is largely based off of this tutorial. I left comments in my C code describing the intent of each section. If you know why my attempt to emulate the SNES's mode 7 isn't working, please let me know. Note: to turn, press the left and right arrow keys, and to move forward and backward, press forward and back. This code may not work on non-Intel systems since I am depending on Intel SIMD intrinsics. I am using clang 12.0.5, if that helps.

Here is my mode_7 function which drives the effect:

void mode_7(const Camera camera, const Screen screen, const Sprite* sprite, const Uint8* keys) {
    static Uint32 local_buffer[height][width];

    const int height_center = height / 2;
    const Vector dimensions = vec_set(sprite -> w);

    for (int z = -height_center; z < height_center; z++) {
        const int y = z + height_center;

        for (int x = 0; x < width; x++) {
            const int reverse_x = width - x;

            const Vector rot_pos_3D = {
                reverse_x * camera.dir[1] + x * camera.dir[0],
                reverse_x * camera.dir[0] - x * camera.dir[1]
            };

            const Vector pos_2D = vec_add(camera.pos, vec_div(rot_pos_3D, vec_set(z)));
            const Vector floor_pos_2D = {floor(pos_2D[0]), floor(pos_2D[1])};
            const Vector tex_pos = vec_mul(vec_sub(pos_2D, floor_pos_2D), dimensions);

            local_buffer[y][x] = read_sprite_pixel(sprite, (long) tex_pos[0], (long) tex_pos[1]);
        }
    }
    memcpy(screen.pixels, local_buffer, width * height * sizeof(Uint32));
}

This is how I am calculating the position, direction, and angle of the camera:

void update_camera(Camera* camera, const Uint8* keys) {
    if (keys[SDL_SCANCODE_LEFT]) {
        if ((camera -> angle -= camera -> v_turn) < 0.0) camera -> angle = two_pi;
    }
    if (keys[SDL_SCANCODE_RIGHT]) {
        if ((camera -> angle += camera -> v_turn) > two_pi) camera -> angle = 0.0;
    }

    camera -> dir = (Vector) {cos(camera -> angle), sin(camera -> angle)};
    const Vector forward_movement = vec_mul(camera -> dir, vec_set(camera -> v_move));
    
    Vector movement = {0.0, 0.0};
    if (keys[SDL_SCANCODE_UP])
        movement = vec_add(movement, forward_movement);
    if (keys[SDL_SCANCODE_DOWN])
        movement = vec_sub(movement, forward_movement);

    camera -> pos = vec_add(camera -> pos, movement);
}

All of the code, including the code above, is below if you want to try it out.

// SDL2 header, handy macros, constants, typedefs

#include <SDL2/SDL.h>

#define FAIL(...) {fprintf(stderr, __VA_ARGS__); exit(1);}

#define vec_set _mm_set1_pd
#define vec_add _mm_add_pd
#define vec_sub _mm_sub_pd
#define vec_mul _mm_mul_pd
#define vec_div _mm_div_pd

const double two_pi = M_PI * 2.0;

enum {
    fps = 60, width = 800, height = 600,
    pixel_format = SDL_PIXELFORMAT_ARGB8888, pixel_format_bpp = 4
};

typedef SDL_Surface Sprite;
typedef __m128d Vector;

typedef struct {
    SDL_Window* window;
    SDL_Renderer* renderer;
    SDL_Texture* buffer;
    SDL_PixelFormat* pixel_format;
    void* pixels;
    int pixel_pitch;
} Screen;

typedef struct {
    Vector pos, dir;
    double angle;
    const double v_move, v_turn;
} Camera;

// abstraction for the screen

Screen init_screen(void) {
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0)
        FAIL("Could not initialize SDL\n");

    SDL_SetHintWithPriority(SDL_HINT_RENDER_VSYNC, "1", SDL_HINT_OVERRIDE);

    Screen screen;
    SDL_CreateWindowAndRenderer(width, height, SDL_RENDERER_ACCELERATED, &screen.window, &screen.renderer);
    screen.buffer = SDL_CreateTexture(screen.renderer, pixel_format, SDL_TEXTUREACCESS_STREAMING, width, height);
    screen.pixel_format = SDL_AllocFormat(pixel_format);

    SDL_SetWindowTitle(screen.window, "Mode 7");
    SDL_SetRenderTarget(screen.renderer, NULL);
    SDL_SetRenderDrawColor(screen.renderer, 0, 0, 0, 0);

    return screen;
}

void deinit_screen(const Screen screen) {
    SDL_DestroyWindow(screen.window);
    SDL_DestroyRenderer(screen.renderer);
    SDL_DestroyTexture(screen.buffer);
    SDL_FreeFormat(screen.pixel_format);
    SDL_Quit();
}

void clear_screen(Screen* const screen) {
    SDL_LockTexture(screen -> buffer, NULL, &screen -> pixels, &screen -> pixel_pitch);
}

void refresh_screen(const Screen screen, const Uint32 before) {
    SDL_UnlockTexture(screen.buffer);
    SDL_RenderCopy(screen.renderer, screen.buffer, NULL, NULL);
    SDL_RenderPresent(screen.renderer);

    const int wait = fps / 1000 - (SDL_GetTicks() - before);
    if (wait > 0) SDL_Delay(wait);
}

// abstraction for sprites

Sprite* init_sprite(const char* const path, const SDL_PixelFormat* pixel_format) {
    SDL_Surface* const unconverted_surface = SDL_LoadBMP(path);
    if (unconverted_surface == NULL) FAIL("Could not load a sprite of path %s\n", path);

    SDL_Surface* const converted_surface = SDL_ConvertSurface(unconverted_surface, pixel_format, 0);
    if (converted_surface == NULL) FAIL("Could not convert a sprite's surface type: %s\n", path);
    SDL_FreeSurface(unconverted_surface);
    SDL_LockSurface(converted_surface);

    return converted_surface;
}

void deinit_sprite(Sprite* sprite) {
    SDL_UnlockSurface(sprite);
    SDL_FreeSurface(sprite);
}

Uint32 read_sprite_pixel(const Sprite* sprite, const int x, const int y) {
    return *(Uint32*) ((Uint8*) sprite -> pixels + y * sprite -> pitch + x * pixel_format_bpp);
}

// the core of my code

void mode_7(const Camera camera, const Screen screen, const Sprite* sprite, const Uint8* keys) {
    static Uint32 local_buffer[height][width];

    const int height_center = height / 2;
    const Vector dimensions = vec_set(sprite -> w);

    for (int z = -height_center; z < height_center; z++) {
        const int y = z + height_center;

        for (int x = 0; x < width; x++) {
            const int reverse_x = width - x;

            const Vector rot_pos_3D = {
                reverse_x * camera.dir[1] + x * camera.dir[0],
                reverse_x * camera.dir[0] - x * camera.dir[1]
            };

            const Vector pos_2D = vec_add(camera.pos, vec_div(rot_pos_3D, vec_set(z)));
            const Vector floor_pos_2D = {floor(pos_2D[0]), floor(pos_2D[1])};
            const Vector tex_pos = vec_mul(vec_sub(pos_2D, floor_pos_2D), dimensions);

            local_buffer[y][x] = read_sprite_pixel(sprite, (long) tex_pos[0], (long) tex_pos[1]);
        }
    }
    memcpy(screen.pixels, local_buffer, width * height * sizeof(Uint32));
}

// input reading + main

void update_camera(Camera* camera, const Uint8* keys) {
    if (keys[SDL_SCANCODE_LEFT]) {
        if ((camera -> angle -= camera -> v_turn) < 0.0) camera -> angle = two_pi;
    }
    if (keys[SDL_SCANCODE_RIGHT]) {
        if ((camera -> angle += camera -> v_turn) > two_pi) camera -> angle = 0.0;
    }

    camera -> dir = (Vector) {cos(camera -> angle), sin(camera -> angle)};
    const Vector forward_movement = vec_mul(camera -> dir, vec_set(camera -> v_move));
    
    Vector movement = {0.0, 0.0};
    if (keys[SDL_SCANCODE_UP])
        movement = vec_add(movement, forward_movement);
    if (keys[SDL_SCANCODE_DOWN])
        movement = vec_sub(movement, forward_movement);

    camera -> pos = vec_add(camera -> pos, movement);
}

int main(void) {
    Screen screen = init_screen();
    Sprite* sprite = init_sprite("../assets/dirt.bmp", screen.pixel_format);
    Camera camera = {{0.0, 0.0}, {0.0, 0.0}, 0.0, 0.1, 0.05};

    const Uint8* keys = SDL_GetKeyboardState(NULL);
    SDL_Event event;
    while (1) {
        const Uint32 before = SDL_GetTicks();
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                deinit_sprite(sprite);
                deinit_screen(screen);
                return 0;
            }
        }
        update_camera(&camera, keys);
        clear_screen(&screen);
        mode_7(camera, screen, sprite, keys);
        refresh_screen(screen, before);
    }   
}

Solution

  • There are two main issues with your code. One part is:

    const Vector rot_pos_3D = {
        reverse_x * camera.dir[1] + x * camera.dir[0],
        reverse_x * camera.dir[0] - x * camera.dir[1]
    };
    

    This part is supposed to does more or less linear interpolation between two points when x changes from 0 to width. One is located at camera.dir pointing forward. The other one is pointing to orthogonal direction {camera.dir[1], -camera.dir[0]}.

    See the image below: enter image description here

    However, it looks that the coordinates are swapped. It should be:

    const Vector rot_pos_3D = {
        reverse_x * camera.dir[0] - x * camera.dir[1],
        reverse_x * camera.dir[1] + x * camera.dir[0],
    };
    

    Now navigation becomes less chaotic. However,the players eye always look to front-left direction as on image above.

    A simple solution to the problem is to place the screen in the front of the player between points (FORWARD+LEFT) and (FORWARD-LEFT) points as on image below.

    enter image description here

    This fix is applied with following patch:

    @@ -107,6 +107,8 @@ void mode_7(const Camera camera, const Screen screen, const Sprite* sprite, cons
         const int height_center = height / 2;
         const Vector dimensions = vec_set(sprite -> w);
     
    +    double d0[2] = {camera.dir[0] + camera.dir[1], camera.dir[1] - camera.dir[0]};
    +    double d1[2] = {camera.dir[0] - camera.dir[1], camera.dir[1] + camera.dir[0]};
         for (int z = -height_center; z < height_center; z++) {
             const int y = z + height_center;
     
    @@ -114,11 +116,11 @@ void mode_7(const Camera camera, const Screen screen, const Sprite* sprite, cons
                 const int reverse_x = width - x;
     
                 const Vector rot_pos_3D = {
    -                reverse_x * camera.dir[1] + x * camera.dir[0],
    -                reverse_x * camera.dir[0] - x * camera.dir[1]
    +                reverse_x * d0[0] + x * d1[0],
    +                reverse_x * d0[1] + x * d1[1],
                 };
     
    
    

    The other issue is "ceiling" moving in the opposite direction. It is caused by dividing by negative "z" in upper part of the screen. Just apply abs():

    -            const Vector pos_2D = vec_add(camera.pos, vec_div(rot_pos_3D, vec_set(z)));
    +            const Vector pos_2D = vec_add(camera.pos, vec_div(rot_pos_3D, vec_set(abs(z))));
    
    

    Now the infinite room should be rendered correctly.