se.llbit.chunky.world.Layer.java Source code

Java tutorial

Introduction

Here is the source code for se.llbit.chunky.world.Layer.java

Source

/* Copyright (c) 2010-2012 Jesper qvist <jesper@llbit.se>
 *
 * This file is part of Chunky.
 *
 * Chunky is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Chunky is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with Chunky.  If not, see <http://www.gnu.org/licenses/>.
 */
package se.llbit.chunky.world;

import java.awt.Graphics;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.io.OutputStream;

import org.apache.commons.math3.util.FastMath;

import se.llbit.chunky.map.RenderBuffer;
import se.llbit.chunky.resources.MiscImages;
import se.llbit.chunky.resources.Texture;
import se.llbit.math.Color;
import se.llbit.math.QuickMath;
import se.llbit.util.ImageTools;

/**
 * A layer describes the visible part of a chunk.
 *
 * A chunk typically stores three layers;
 * current layer, cave layer and surface layer.
 *
 * @author Jesper qvist (jesper@llbit.se)
 */
public class Layer {

    private byte[] blocks = null;
    private int[] bitmap = null;
    private int avgColor = 0xFF;
    static int selectionColor = 0x75FF0000;

    /**
     * Singleton instance of empty layer.
     */
    public static final Layer emptyLayer = new Layer() {

        @Override
        public synchronized void renderTopography(ChunkPosition position, Heightmap heightmap) {
        }

        @Override
        public synchronized void render(RenderBuffer rbuff, int cx, int cz) {
            ChunkView view = rbuff.getView();
            int x0 = view.chunkScale * (cx - view.ix0);
            int z0 = view.chunkScale * (cz - view.iz0);

            if (view.chunkScale == 1) {
                rbuff.setRGB(x0, z0, averageColor);
            } else {
                rbuff.fillRect(x0, z0, view.chunkScale, view.chunkScale, averageColor);
            }
        }

        private final int averageColor = 0xFF000000;

        @Override
        public int getAvgColor() {
            return averageColor;
        }

    };

    /**
     * An unknown layer is a layer that has not yet been parsed
     */
    public static final Layer unknownLayer = new Layer() {

        @Override
        public synchronized void renderTopography(ChunkPosition position, Heightmap heightmap) {
        }

        @Override
        public synchronized void render(RenderBuffer rbuff, int cx, int cz) {
            ChunkView view = rbuff.getView();
            int x0 = view.chunkScale * (cx - view.ix0);
            int z0 = view.chunkScale * (cz - view.iz0);

            if (view.chunkScale == 1) {
                rbuff.setRGB(x0, z0, averageColor);
            } else {
                Graphics g = rbuff.getGraphics();
                g.drawImage(MiscImages.load, x0, z0, view.chunkScale, view.chunkScale, null);
            }
        }

        private final int averageColor = ImageTools.calcAvgColor(MiscImages.load);

        @Override
        public int getAvgColor() {
            return averageColor;
        }
    };

    /**
     * Represents corrupt chunk data
     */
    public static final Layer corruptLayer = new Layer() {
        @Override
        public synchronized void renderTopography(ChunkPosition position, Heightmap heightmap) {
        }

        @Override
        public synchronized void render(RenderBuffer rbuff, int cx, int cz) {
            ChunkView view = rbuff.getView();
            int x0 = view.chunkScale * (cx - view.ix0);
            int z0 = view.chunkScale * (cz - view.iz0);

            if (view.chunkScale == 1) {
                rbuff.setRGB(x0, z0, averageColor);
            } else {
                rbuff.getGraphics().drawImage(MiscImages.corruptLayer, x0, z0, view.chunkScale, view.chunkScale,
                        null);
            }
        }

        private final int averageColor = ImageTools.calcAvgColor(MiscImages.corruptLayer);

        @Override
        public int getAvgColor() {
            return averageColor;
        }
    };

    /**
     * Creates a new tile layer
     * @param data tile data
     */
    private Layer(byte[] data) {
        blocks = data;
    }

    /**
     * Creates a new bitmap layer
     * @param data bitmap data
     */
    private Layer(int[] data) {
        bitmap = data;
        avgColor = avgBitmapColor();
    }

    private int avgBitmapColor() {
        float[] avg = new float[3];
        float[] frgb = new float[3];
        for (int i = 0; i < 16 * 16; ++i) {
            Color.getRGBComponents(bitmap[i], frgb);
            avg[0] += frgb[0];
            avg[1] += frgb[1];
            avg[2] += frgb[2];
        }
        return Color.getRGB(avg[0] / (16 * 16), avg[1] / (16 * 16), avg[2] / (16 * 16));
    }

    /**
     * Create an empty layer.
     */
    public Layer() {
    }

    /**
     * Render block highlight
     * @param rbuff
     * @param cx
     * @param cz
     * @param hlBlock
     * @param highlight
     */
    public synchronized void renderHighlight(RenderBuffer rbuff, int cx, int cz, Block hlBlock,
            java.awt.Color highlight) {

        ChunkView view = rbuff.getView();
        int x0 = view.chunkScale * (cx - view.ix0);
        int z0 = view.chunkScale * (cz - view.iz0);

        if (blocks == null)
            return;

        Graphics g = rbuff.getGraphics();
        g.setColor(new java.awt.Color(1, 1, 1, 0.35f));
        g.fillRect(x0, z0, view.chunkScale, view.chunkScale);
        g.setColor(highlight);

        if (view.chunkScale == 16) {

            for (int x = 0; x < 16; ++x) {
                int xp = x0 + x;

                for (int z = 0; z < 16; ++z) {
                    int yp = z0 + z;

                    if (hlBlock.id == (0xFF & blocks[x * 16 + z])) {
                        rbuff.setRGB(xp, yp, highlight.getRGB());
                    }
                }
            }
        } else {
            int blockScale = view.chunkScale / 16;

            for (int x = 0; x < 16; ++x) {
                int xp0 = x0 + x * blockScale;
                int xp1 = xp0 + blockScale;

                for (int z = 0; z < 16; ++z) {
                    int yp0 = z0 + z * blockScale;
                    int yp1 = yp0 + blockScale;

                    if (hlBlock.id == (0xFF & blocks[x * 16 + z])) {
                        g.fillRect(xp0, yp0, xp1 - xp0, yp1 - yp0);
                    }
                }
            }
        }
    }

    /**
     * Render this layer
     * @param rbuff
     * @param cx
     * @param cz
     */
    public synchronized void render(RenderBuffer rbuff, int cx, int cz) {

        if (blocks != null) {
            renderTiles(rbuff, cx, cz);
        } else if (bitmap != null) {
            renderBitmap(rbuff, cx, cz);
        }
    }

    private void renderTiles(RenderBuffer rbuff, int cx, int cz) {

        ChunkView view = rbuff.getView();
        int blockScale = view.chunkScale / 16;
        int x0 = view.chunkScale * (cx - view.ix0);
        int z0 = view.chunkScale * (cz - view.iz0);

        if (view.chunkScale == 1) {

        } else if (blockScale == 1) {

            for (int z = 0; z < 16; ++z) {
                int yp = z0 + z;

                for (int x = 0; x < 16; ++x) {
                    int xp = x0 + x;

                    byte block = blocks[x * 16 + z];
                    if (block == Block.AIR.id) {
                        rbuff.setRGB(xp, yp, 0xFFFFFFFF);
                    } else {
                        rbuff.setRGB(xp, yp, Block.get(block).getAvgRGB());
                    }
                }
            }
        } else if (blockScale < 12) {

            for (int z = 0; z < 16; ++z) {
                int yp0 = z0 + z * blockScale;

                for (int x = 0; x < 16; ++x) {
                    int xp0 = x0 + x * blockScale;

                    byte block = blocks[x * 16 + z];
                    if (block == Block.AIR.id) {
                        rbuff.fillRect(xp0, yp0, blockScale, blockScale, 0xFFFFFFFF);
                    } else {
                        rbuff.fillRect(xp0, yp0, blockScale, blockScale, Block.get(block).getAvgRGB());
                    }
                }
            }
        } else {
            for (int z = 0; z < 16; ++z) {
                int yp0 = z0 + z * blockScale;

                for (int x = 0; x < 16; ++x) {
                    int xp0 = x0 + x * blockScale;

                    byte block = blocks[x * 16 + z];
                    if (block == Block.AIR.id) {
                        rbuff.fillRect(xp0, yp0, blockScale, blockScale, 0xFFFFFFFF);
                        continue;
                    }

                    int[] tex = ((DataBufferInt) Block.get(block).getTexture().getScaledImage(blockScale)
                            .getRaster().getDataBuffer()).getData();
                    for (int i = 0; i < blockScale; ++i) {
                        for (int j = 0; j < blockScale; ++j) {
                            rbuff.setRGB(xp0 + j, yp0 + i, tex[j + blockScale * i]);
                        }
                    }
                }
            }
        }
    }

    private void renderBitmap(RenderBuffer rbuff, int cx, int cz) {

        ChunkView view = rbuff.getView();
        int x0 = view.chunkScale * (cx - view.ix0);
        int z0 = view.chunkScale * (cz - view.iz0);

        if (view.chunkScale == 1) {
            rbuff.setRGB(x0, z0, avgColor);
        } else if (view.chunkScale == 16) {

            for (int z = 0; z < 16; ++z) {
                for (int x = 0; x < 16; ++x) {
                    rbuff.setRGB(x0 + x, z0 + z, bitmap[z + x * 16]);
                }
            }
        } else {

            int blockScale = view.chunkScale / 16;

            for (int z = 0; z < 16; ++z) {
                int yp0 = z0 + z * blockScale;

                for (int x = 0; x < 16; ++x) {
                    int xp0 = x0 + x * blockScale;

                    rbuff.fillRect(xp0, yp0, blockScale, blockScale, bitmap[z + x * 16]);
                }
            }
        }
    }

    /**
     * Load layer from block data
     * @param blockData
     * @param layer
     * @return The loaded layer
     */
    public static Layer loadLayer(byte[] blockData, int layer) {
        byte[] data = new byte[16 * 16];
        for (int x = 0; x < 16; ++x) {
            for (int z = 0; z < 16; ++z) {
                data[x * 16 + z] = blockData[Chunk.chunkIndex(x, layer, z)];
            }
        }
        return new Layer(data);
    }

    /**
     * Load biome IDs into layer
     * @param chunkBiomes
     * @return The loaded layer
     */
    public static Layer loadBiomes(byte[] chunkBiomes) {
        int[] surface = new int[16 * 16];
        for (int x = 0; x < 16; ++x) {
            for (int z = 0; z < 16; ++z) {
                surface[x * 16 + z] = Biomes.getColor(0xFF & chunkBiomes[Chunk.chunkXZIndex(x, z)]);
            }
        }
        return new Layer(surface);
    }

    /**
     * Generate the surface bitmap.
     *
     * @param dim current dimension
     * @param position Chunk position
     * @param blocksArray block id array
     * @param biomes
     * @param blockData
     * @return the generated bitmap layer
     */
    public static Layer loadSurface(int dim, ChunkPosition position, byte[] blocksArray, byte[] biomes,
            byte[] blockData) {

        int[] surface = new int[16 * 16];
        for (int x = 0; x < 16; ++x) {
            for (int z = 0; z < 16; ++z) {

                // find the topmost non-empty block
                int y = Chunk.Y_MAX - 1;
                if (dim != -1) {
                    for (; y > 0; --y) {
                        int block = 0xFF & blocksArray[Chunk.chunkIndex(x, y, z)];
                        if (block != Block.AIR.id)
                            break;
                    }
                } else {
                    // nether worlds have a ceiling that we want to skip
                    for (; y > 1; --y) {
                        int block = 0xFF & blocksArray[Chunk.chunkIndex(x, y, z)];
                        if (block != Block.AIR.id)
                            break;
                    }
                    for (; y > 1; --y) {
                        int block = 0xFF & blocksArray[Chunk.chunkIndex(x, y, z)];
                        if (block == Block.AIR.id)
                            break;
                    }
                    for (; y > 1; --y) {
                        int block = 0xFF & blocksArray[Chunk.chunkIndex(x, y, z)];
                        if (block != Block.AIR.id)
                            break;
                    }
                }

                float[] color = new float[4];

                colorloop: for (; y >= 0 && color[3] < 1.f;) {
                    Block block = Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]);
                    float[] blockColor = new float[4];
                    int biomeId = 0xFF & biomes[Chunk.chunkXZIndex(x, z)];

                    switch (block.id) {

                    case Block.LEAVES_ID:
                        Color.getRGBComponents(Biomes.getFoliageColor(biomeId), blockColor);
                        blockColor[3] = 1.f;// foliage colors don't include alpha

                        y -= 1;
                        break;

                    case Block.GRASS_ID:
                    case Block.VINES_ID:
                    case Block.TALLGRASS_ID:
                        Color.getRGBComponents(Biomes.getGrassColor(biomeId), blockColor);
                        blockColor[3] = 1.f;// grass colors don't include alpha

                        y -= 1;
                        break;

                    case Block.STAINED_CLAY_ID:
                        int clayType = 0xFF & blockData[Chunk.chunkIndex(x, y, z) / 2];
                        clayType >>= (x % 2) * 4;
                        clayType &= 0xF;

                        Color.getRGBComponents(Texture.stainedClay[clayType].getAvgColor(), blockColor);
                        blockColor[3] = 1.f;// wool colors don't include alpha

                        y -= 1;
                        break;

                    case Block.CARPET_ID:
                    case Block.WOOL_ID:
                        int woolType = 0xFF & blockData[Chunk.chunkIndex(x, y, z) / 2];
                        woolType >>= (x % 2) * 4;
                        woolType &= 0xF;

                        Color.getRGBComponents(Texture.wool[woolType].getAvgColor(), blockColor);
                        blockColor[3] = 1.f;// wool colors don't include alpha

                        y -= 1;
                        break;

                    case Block.ICE_ID:
                        Color.getRGBAComponents(Block.ICE.getAvgTopRGB(), blockColor);
                        color = blend(color, blockColor);
                        y -= 1;

                        for (; y >= 0; --y) {
                            if (Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]).isOpaque) {
                                Color.getRGBAComponents(block.getAvgTopRGB(), blockColor);
                                break;
                            }
                        }
                        break;

                    case Block.WATER_ID:
                    case Block.STATIONARYWATER_ID:
                        int depth = 1;
                        y -= 1;
                        for (; y >= 0; --y) {
                            Block block1 = Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]);
                            if (!block1.isWater())
                                break;
                            depth += 1;
                        }

                        Color.getRGBAComponents(Block.WATER.getAvgTopRGB(), blockColor);
                        blockColor[3] = QuickMath.max(.5f, 1.f - depth / 32.f);
                        break;

                    default:
                        Color.getRGBAComponents(block.getAvgTopRGB(), blockColor);

                        if (block.isOpaque && y > 64) {
                            float fade = QuickMath.min(0.6f, (y - World.SEA_LEVEL) / 60.f);
                            fade = QuickMath.max(0.f, fade);
                            blockColor[0] = (1 - fade) * blockColor[0] + fade;
                            blockColor[1] = (1 - fade) * blockColor[1] + fade;
                            blockColor[2] = (1 - fade) * blockColor[2] + fade;
                        }

                        y -= 1;
                        break;
                    }

                    color = blend(color, blockColor);

                    if (block.isOpaque)
                        break colorloop;
                }

                surface[x * 16 + z] = Color.getRGBA(color[0], color[1], color[2], color[3]);
            }
        }

        return new Layer(surface);
    }

    /**
     * Blend the two argb colors a and b. Result is stored in the array a.
     * @param src
     * @param dst
     */
    private static final float[] blend(float[] src, float[] dst) {
        float[] out = new float[4];
        out[3] = src[3] + dst[3] * (1 - src[3]);
        out[0] = (src[0] * src[3] + dst[0] * dst[3] * (1 - src[3])) / out[3];
        out[1] = (src[1] * src[3] + dst[1] * dst[3] * (1 - src[3])) / out[3];
        out[2] = (src[2] * src[3] + dst[2] * dst[3] * (1 - src[3])) / out[3];
        return out;
    }

    /**
     * Add topographical gradient to this chunk and calculate average color
     * @param position
     * @param heightmap
     */
    public synchronized void renderTopography(ChunkPosition position, Heightmap heightmap) {

        int cx = position.x * Chunk.X_MAX;
        int cz = position.z * Chunk.Z_MAX;

        float[] rgb = new float[3];
        for (int x = 0; x < 16; ++x) {

            for (int z = 0; z < 16; ++z) {

                Color.getRGBComponents(bitmap[x * 16 + z], rgb);

                float gradient = (heightmap.get(cx + x, cz + z) + heightmap.get(cx + x + 1, cz + z)
                        + heightmap.get(cx + x, cz + z + 1) - heightmap.get(cx + x - 1, cz + z)
                        - heightmap.get(cx + x, cz + z - 1) - heightmap.get(cx + x - 1, cz + z - 1));
                gradient = (float) ((FastMath.atan(gradient / 15) / (Math.PI / 1.7)) + 1);

                rgb[0] *= gradient;
                rgb[1] *= gradient;
                rgb[2] *= gradient;

                // clip the result
                rgb[0] = QuickMath.max(0.f, rgb[0]);
                rgb[0] = QuickMath.min(1.f, rgb[0]);
                rgb[1] = QuickMath.max(0.f, rgb[1]);
                rgb[1] = QuickMath.min(1.f, rgb[1]);
                rgb[2] = QuickMath.max(0.f, rgb[2]);
                rgb[2] = QuickMath.min(1.f, rgb[2]);

                bitmap[x * 16 + z] = Color.getRGB(rgb[0], rgb[1], rgb[2]);
            }
        }
    }

    /**
     * Generate the cave map. Only holes which are large enough to have mobs spawn
     * in them are counted towards the color of a cave. The color is then determined
     * by the number of unoccupied blocks beneath the topmost (surface) block.
     *
     * The more empty space, the deeper the cave color.
     *
     * @param blocksArray
     * @param heightmap
     * @return The loaded layer
     */
    public static Layer loadCaves(byte[] blocksArray, int[] heightmap) {
        int[] caves = new int[16 * 16];
        for (int x = 0; x < 16; ++x) {
            for (int z = 0; z < 16; ++z) {
                int y = heightmap[z * 16 + x];
                y = Math.max(0, y - 1);

                // find ground level
                for (; y > 1; --y) {
                    int block = blocksArray[Chunk.chunkIndex(x, y, z)] & 0xFF;
                    if (block != Block.AIR.id && block != Block.LEAVES.id && block != Block.WOOD.id)
                        break;
                }

                // find caves
                int luftspalt = 0;
                for (; y > 1; --y) {
                    Block block = Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]);
                    if (block.isCave()) {
                        y -= 1;
                        Block block1 = Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]);
                        if (block1.isCave()) {
                            luftspalt++;
                            y -= 1;
                        }
                    }
                }

                if (luftspalt == 0) {
                    caves[x * 16 + z] = 0xFFFFFFFF;
                } else {
                    luftspalt = Math.min(64, luftspalt * 3 + 5);
                    float fade = QuickMath.max(0, 1.f - luftspalt / 64.f);
                    caves[x * 16 + z] = Color.getRGB(1.f * fade, 1.f * fade, 1.f);
                }
            }
        }
        return new Layer(caves);
    }

    /**
     * Load heightmap information from a chunk heightmap array
     * and insert into a quadtree.
     *
     * @param heightmap
     * @param pos
     * @param blocksArray
     * @param chunkHeightmap
     */
    public static void updateHeightmap(Heightmap heightmap, ChunkPosition pos, byte[] blocksArray,
            int[] chunkHeightmap) {
        for (int x = 0; x < 16; ++x) {
            for (int z = 0; z < 16; ++z) {
                int y = chunkHeightmap[z * 16 + x];
                y = Math.max(1, y - 1);
                for (; y > 1; --y) {
                    Block block = Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]);
                    if (block != Block.AIR && !block.isWater())
                        break;
                }
                heightmap.set(y, pos.x * 16 + x, pos.z * 16 + z);
            }
        }
    }

    /**
     * @return The average color of this layer
     */
    public int getAvgColor() {
        return avgColor;
    }

    /**
     * Write a PNG scanline
     * @param scanline
     * @param out
     * @throws IOException
     */
    public void writePngLine(int scanline, OutputStream out) throws IOException {
        if (bitmap != null) {
            byte[] rgb = new byte[] { 0, 0, 0 };
            for (int x = 15; x >= 0; --x) {
                int rgbInt = bitmap[x + scanline * 16];
                rgb[0] = (byte) (rgbInt >> 16);
                rgb[1] = (byte) (rgbInt >> 8);
                rgb[2] = (byte) rgbInt;
                out.write(rgb);
            }
        } else {
            byte[] white = new byte[] { -1, -1, -1 };
            byte[] black = new byte[] { 0, 0, 0 };
            for (int x = 0; x < 16; ++x) {
                if (x == scanline)
                    out.write(black);
                else
                    out.write(white);
            }
        }
    }

}