src/Terrain.cc
author Tero Marttila <terom@fixme.fi>
Tue, 20 Jan 2009 23:30:18 +0200
changeset 408 e6cfc44266af
parent 406 a2e35ca66c74
child 409 1a03ff151abc
permissions -rw-r--r--
reorganize Terrain/PhysicsWorld/GameState/Engine to use NetworkClientConnect, and hence handle the connection process asynchronously, and finally properly implement receiving the terrain data from the server
#include "Terrain.hh"
#include "Graphics.hh"
#include "Engine.hh"

#include <cmath>
#include <cassert>
#include <algorithm>
#include <ClanLib/display.h>

const Vector DIRECTIONS[] = {
    Vector(0,-1),
    Vector(1,-1),
    Vector(1,0),
    Vector(1,1),
    Vector(0,1),
    Vector(-1,1),
    Vector(-1,0),
    Vector(-1,-1)
};

Terrain::Terrain (void) :
    terrain_buf(NULL), width(0), height(0)
{
}

Terrain::Terrain (PixelDimension width, PixelDimension height, int seed) :
    terrain_buf(NULL),
    width(width), height(height)
{
    // allocate+generate random terrain
    generateTerrain(seed);
}

Terrain::Terrain (PixelDimension width, PixelDimension height, TerrainPixel *terrain_buf) :
    terrain_buf(terrain_buf),
    width(width), height(height)
{
    // just generate the pixel buffer
    generatePixelBuffer();
}

Terrain::~Terrain (void) {
    // free terrain data
    delete[] terrain_buf;
}

void Terrain::generateTerrain (int seed) {
    // shouldn't be generated yet
    assert(!terrain_buf);

    // set random number generator seed.
    srand(seed); 

    // allocate terrain buffer
    terrain_buf = new TerrainPixel[width * height];

    // fill with dirt
    memset(terrain_buf, TERRAIN_DIRT, width * height);

    // some constants to control random generation
    const int min_range = 25;
    const int max_range = 80;
    const int num = 50;
    const int rock_rarity = 4;

    // generate \a num random circles
    for (int i = 0; i < num; i++) {
        // circle origin
        PixelCoordinate mid (rand() % width, rand() % height);

        // radius
        int range = rand() % (max_range - min_range) + min_range;

        // circle type
        TerrainType type = TERRAIN_EMPTY;

        // tweak to make sure that there's a circle in the midle of the cave
        if (i == 0) {
            mid.x = width / 2;
            mid.y = height / 2;
            range = 150;

        } else if (rand() % rock_rarity == 0) {
            // some rock
           type = TERRAIN_ROCK;
        }

        // iterate over the area of the circle
        for (
            PixelDimension y = std::max((PixelDimension) 0, mid.y - range);
            y < std::min(height, mid.y + range);
            y++
        ) {
            for (
                PixelDimension x = std::max((PixelDimension) 0, mid.x - range); 
                x < std::min(width, mid.x + range); 
                x++
            ) {
                // inside radius?
                if ((x - mid.x) * (x - mid.x) + (y - mid.y) * (y - mid.y) < range * range) 
                    terrain_buf[y * width + x] = (TerrainPixel) type;
            }
        } 
    }
    
    // update pixel buffer
    generatePixelBuffer();
}

void Terrain::generatePixelBuffer (void) {
    // initialize textures
    generateTexture();

    // create the pixel buffer of the correct size
    pixbuf = CL_PixelBuffer(width, height, 4 * width, CL_PixelFormat::rgba8888);

    // iterate over each pixel
    for (PixelDimension x = 0; x < width; x++) {
        for (PixelDimension y = 0; y < height; y++) {
            // draw textureized pixel color
            pixbuf.draw_pixel(x, y, getTexturePixel(x, y));
        }
    }
}

/*
 * Texture generation utility functions
 */
static void fractal_step (std::vector<double>& land, int size, double str, int dist) {
    for (int i = 0; i < size; i += dist * 2) {
        for (int j = dist; j < size; j += dist * 2) {
            double sum = 0;
            sum += land[(i + size - dist) % size + j * size];
            sum += land[i + ((j + size - dist) % size) * size];
            sum += land[(i + dist) % size + j * size];
            sum += land[i + ((j + dist) % size) * size];
            land[i + j * size] = sum / 4 + (rand() % 10000 - 5000) * str;
        }
    }
    for (int i = dist; i < size; i += dist * 2) {
        for (int j = 0; j < size; j += dist * 2) {
            double sum = 0;
            sum += land[(i + size - dist) % size + j * size];
            sum += land[i + ((j + size - dist) % size) * size];
            sum += land[(i + dist) % size + j * size];
            sum += land[i + ((j + dist)  % size) * size];
            land[i + j * size] = sum / 4 + (rand() % 10000 - 5000) * str;
        }
    }
}

static void fractal_diamond (std::vector<double>& land, int size, double str, int dist) {
    for (int i = dist; i < size; i += dist*2) {
        for (int j = dist; j < size; j += dist*2) {
            double sum = 0;
            sum += land[(i + size - dist) % size + ((j + size - dist) % size) * size];
            sum += land[(i + dist) % size + ((j + size - dist) % size) * size];
            sum += land[(i + size - dist) % size + ((j + dist) % size) * size];
            sum += land[(i + dist) % size + ((j + dist) % size) * size];
            land[i + j * size] = sum / 4 + (rand() % 10000 - 5000) * str;
        }
    }
}

void Terrain::generateTexture (void) {
    // texture is a 128x128 px pattern
    int texturesize = 128;
    
    // store the generated texture
    texture = std::vector<std::vector<int> >(texturesize, std::vector<int>(texturesize));
    
    // generate texture into this
    std::vector<double> land(texture.size() * texture.size());

    // XXX: magic constants with unintelligble names
    double str = 0.8;
    double H = 0.8;
    
    // do some magic
    for (int i = 512; i >= 1; i /= 2) {
        fractal_diamond(land, texturesize, str, i);
        fractal_step(land, texturesize, str, i);
        str *= H;
    }

    double min = 100000;
    double max = -100000;
    
    // find the range of minimum and maximum values
    for (int i = 0; i < texturesize * texturesize; i++) {
        if (land[i] < min) min = land[i];
        if (land[i] > max) max = land[i];
    }
    
    // normalize min down to zero
    max -= min;
    
    // normalize values to [0, max]
    for (int i = 0; i < texturesize * texturesize; i++) {
        land[i] -= min;
    }
    
    // copy+mangle land to texture as integers
    for (int i = 0; i < texturesize * texturesize; i++) {
        land[i] = land[i] * 255 / max;
        texture[i % texturesize][i / texturesize] = (int) land[i];
    }
}

static int normalizeRange (int min, int val, int max) {
    if (val < min)
        return min;

    else if (val > max)
        return max;

    else 
        return val;
}

CL_Color Terrain::getTexturePixel (PixelDimension x, PixelDimension y) {
    CL_Color color;
    int texture_fade = 8;
    
    // determine fade constant
    switch (getType(x, y)) {
        case TERRAIN_EMPTY:
            color = COLOR_EMPTY;
            break;

        case TERRAIN_DIRT:
            color = COLOR_DIRT;
            texture_fade = 4;
            break;

        case TERRAIN_ROCK:
            color = COLOR_ROCK;
            texture_fade = 2;
            break;
    }
    
    // calculate texture coordinate
    int tx = (x + texture_fade * 37) % texture.size();
    int ty = (y + texture_fade * 37) % texture[0].size();
    
    // get color components
    int red = color.get_red();
    int green = color.get_green();
    int blue = color.get_blue();
    
    // apply noise from texture
    red += (texture[tx][ty] - 128) / texture_fade;
    green += (texture[tx][ty] - 128) / texture_fade;
    blue += (texture[tx][ty] - 128) / texture_fade;
    
    // normalize colors to within range and return new color    
    return CL_Color(
            normalizeRange(0, red, 255),
            normalizeRange(0, green, 255),
            normalizeRange(0, blue, 255)
    );
}

const TerrainPixel* Terrain::getTerrainBuffer (void) const {
    return terrain_buf;
}
    
void Terrain::loadFromBuffer (const TerrainPixel *buf) {
    // copy bytes
    memcpy(terrain_buf, buf, width * height);

    // regenerate pixbuf
    generatePixelBuffer();
}

bool Terrain::collides (const Vector &point) const {
    return (getType(point) != TERRAIN_EMPTY);
}

bool Terrain::collides (const Vector &begin, const Vector &end) const {
    // TODO: Maybe there should be another function that also returns the point where we collided.

    // use Bresenhams line algorithm to go trough all the "pixels" of the line.
    PixelCoordinate b = getPixelCoordinate(begin);
    PixelCoordinate e = getPixelCoordinate(end);

    // steepness of line (large angle against horizontal)
    bool steep = (abs(e.y - b.y) > abs(e.x - b.x)); // k > 1
    
    // if the line is steep, swap the x and y coordinates
    if (steep) { 
        std::swap(b.x, b.y);
        std::swap(e.x, e.y);
    }
    
    // normalize so that the line goes upwards
    if (b.x > e.x)
        std::swap(b, e);
    
    // line length
    PixelDimension dx = e.x - b.x, dy = abs(e.y - b.y);
    PixelDimension err = dx / 2;
    PixelDimension y = b.y;
    
    // ascending or descending line?
    PixelDimension ystep = b.y < e.y ? 1 : -1;

    // iterate through the pixels on the line
    for (PixelDimension x = b.x; x <= e.x; x++) {
        if (steep) { 
            // x and y coordinates must be switched if steep
            if (getType(y, x) != TERRAIN_EMPTY)
                return true;

        } else {
            if (getType(x, y) != TERRAIN_EMPTY) 
                return true;

        }

        err = err - dy;

        if (err < 0) { 
            // check if we want to make an ystep
            y = y + ystep;
            err = err + dx;
        }
    }

    // no collision
    return false; 
}

void Terrain::removeGround (const Vector &pos, float radius) {
    // scale arguments to pixel units
    PixelCoordinate mid = getPixelCoordinate(pos);
    PixelDimension r = scale(radius); 
    
    // iterate through the circle's pixels
    for (PixelDimension j = mid.y - r; j < mid.y + r; j++) {
        for (PixelDimension i = mid.x - r; i < mid.x + r; i++) {
            // getType also returns TERRAIN_ROCK if out-of-bounds
            if (getType(i, j) != TERRAIN_ROCK) { 

                // within radius?
                if ((i - mid.x) * (i - mid.x) + (j - mid.y) * (j - mid.y) < r * r) {
                    // update terrain buf
                    setType(i, j, TERRAIN_EMPTY);
                }
            }
        }
    }
}

/**
 * Gets the index of the given coordinate direction referring to the DIRECTIONS table in Physics.hh
 *
 * XXX: ugly little "lookup table"
 */
static int getDirectionIndex (Vector direction) {
    Vector dir = direction.roundToInt();

    if (dir.x == 0 && dir.y == -1) {
        return 0;
    } else if (dir.x == 1 && dir.y == -1) {
        return 1;
    } else if (dir.x == 1 && dir.y == 0) {
        return 2;
    } else if (dir.x == 1 && dir.y == 1) {
        return 3;
    } else if (dir.x == 0 && dir.y == 1) {
        return 4;
    } else if (dir.x == -1 && dir.y == 1) {
        return 5;
    } else if (dir.x == -1 && dir.y == 0) {
        return 6;
    } else if (dir.x == -1 && dir.y == -1) {
        return 7;
    }

    Engine::log(DEBUG, "Terrain.getDirectionIndex ") << "invalid direction: " << direction;
    return 0;
}

Vector Terrain::getNormal(Vector point, Vector prevPoint) const {
    // convert location to coordinate
    PixelCoordinate p = getPixelCoordinate(point);
    
    // sanity check
    assert(point != prevPoint);
    
    // round and subtract to get an integer direction vector, and turn this into a direction index
    int dirIdx = getDirectionIndex(prevPoint.roundToInt() - point.roundToInt());
    
    // always add our own direction to normal
    Vector normal = DIRECTIONS[dirIdx];
    
    // check the two pixels clockwise from the impact direction
    for (int i = 1; i <= 2; i++) {
        if (getType(point + DIRECTIONS[(dirIdx + i + 8) % 8]) == TERRAIN_EMPTY) {
            normal += DIRECTIONS[(dirIdx + i + 8) % 8];
        }
    }
    
    // check the two pixels counterclockwise from the impact direction
    for (int i = 1; i <= 2; i++) {
        if (getType(point + DIRECTIONS[(dirIdx - i + 8) % 8]) == TERRAIN_EMPTY) {
            normal += DIRECTIONS[(dirIdx - i + 8) % 8];
        }
    }
    
    // sanity check
    if (getType(point) == TERRAIN_EMPTY || getType(prevPoint) != TERRAIN_EMPTY) {
        Engine::log(DEBUG, "Physics.getNormal ") << "silly collision";
    }
    
    return normal;
}

void Terrain::draw (Graphics *g, PixelCoordinate camera) {
    // XXX: can we optimize this somehow?
    
    // load the terrain pixbuf as a surface
    CL_Surface surf (pixbuf);
    
    // draw it onto the graphics, offset by camera position
    surf.draw(-camera.x, -camera.y, g->get_gc());
}