Package org.terasology.world.block.loader

Source Code of org.terasology.world.block.loader.WorldAtlasImpl

/*
* Copyright 2013 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.world.block.loader;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.math.IntMath;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import gnu.trove.procedure.TObjectIntProcedure;
import org.newdawn.slick.opengl.PNGDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.asset.AssetManager;
import org.terasology.asset.AssetType;
import org.terasology.asset.AssetUri;
import org.terasology.asset.Assets;
import org.terasology.engine.paths.PathManager;
import org.terasology.math.Rect2f;
import org.terasology.math.TeraMath;
import org.terasology.naming.Name;
import org.terasology.registry.CoreRegistry;
import org.terasology.rendering.assets.atlas.Atlas;
import org.terasology.rendering.assets.atlas.AtlasData;
import org.terasology.rendering.assets.material.Material;
import org.terasology.rendering.assets.material.MaterialData;
import org.terasology.rendering.assets.texture.Texture;
import org.terasology.rendering.assets.texture.TextureData;
import org.terasology.rendering.assets.texture.subtexture.SubtextureData;

import javax.imageio.ImageIO;
import javax.vecmath.Vector2f;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;

/**
* @author Immortius
*/
public class WorldAtlasImpl implements WorldAtlas {
    private static final Logger logger = LoggerFactory.getLogger(WorldAtlasImpl.class);

    private static final int MAX_TILES = 65536;
    private static final Color UNIT_Z_COLOR = new Color(0.5f, 0.5f, 1.0f, 1.0f);
    private static final Color TRANSPARENT_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.0f);
    private static final Color BLACK_COLOR = new Color(0.0f, 0.0f, 0.0f, 1.0f);

    private int maxAtlasSize = 4096;
    private int atlasSize = 256;
    private int tileSize = 16;

    private TObjectIntMap<AssetUri> tileIndexes = new TObjectIntHashMap<>();
    private List<TileData> tiles = Lists.newArrayList();
    private List<TileData> tilesNormal = Lists.newArrayList();
    private List<TileData> tilesHeight = Lists.newArrayList();

    private TileData defaultNormal;
    private TileData defaultHeight;

    /**
     * @param maxAtlasSize The maximum dimensions of the atlas (both width and height, in pixels)
     */
    public WorldAtlasImpl(int maxAtlasSize) {
        this.maxAtlasSize = maxAtlasSize;
        for (AssetUri tile : Assets.list(AssetType.BLOCK_TILE)) {
            indexTile(tile);
        }
        buildAtlas();
    }

    @Override
    public int getTileSize() {
        return tileSize;
    }

    @Override
    public int getAtlasSize() {
        return atlasSize;
    }

    @Override
    public float getRelativeTileSize() {
        return ((float) getTileSize()) / (float) getAtlasSize();
    }

    @Override
    public int getNumMipmaps() {
        return TeraMath.sizeOfPower(tileSize) + 1;
    }

    /**
     * Obtains the tex coords of a block tile. If it isn't part of the atlas it is added to the atlas.
     *
     * @param uri         The uri of the block tile of interest.
     * @param warnOnError Whether a warning should be logged if the asset canot be found
     * @return The tex coords of the tile in the atlas.
     */
    @Override
    public Vector2f getTexCoords(AssetUri uri, boolean warnOnError) {
        return getTexCoords(getTileIndex(uri, warnOnError));
    }

    private Vector2f getTexCoords(int id) {
        int tilesPerDim = atlasSize / tileSize;
        return new Vector2f((id % tilesPerDim) * getRelativeTileSize(), (id / tilesPerDim) * getRelativeTileSize());
    }

    private int getTileIndex(AssetUri uri, boolean warnOnError) {
        if (tileIndexes.containsKey(uri)) {
            return tileIndexes.get(uri);
        }
        if (warnOnError) {
            logger.warn("Tile {} could not be resolved", uri);
        }
        return 0;
    }

    private int indexTile(AssetUri uri) {
        if (tiles.size() == MAX_TILES) {
            logger.error("Maximum tiles exceeded");
            return 0;
        }
        TileData tile = CoreRegistry.get(AssetManager.class).loadAssetData(uri, TileData.class);
        if (tile != null) {
            if (checkTile(tile)) {
                int index = tiles.size();
                tiles.add(tile);
                addNormal(uri);
                addHeightMap(uri);
                tileIndexes.put(uri, index);
                return index;
            } else {
                logger.error("Invalid tile {}, must be a square with power-of-two sides.", uri);
                return 0;
            }
        }
        return 0;
    }

    private boolean checkTile(TileData tile) {
        return tile.getImage().getWidth() == tile.getImage().getHeight()
                && IntMath.isPowerOfTwo(tile.getImage().getWidth());
    }

    private void addNormal(AssetUri uri) {
        String name = uri.toSimpleString() + "Normal";
        TileData tile = CoreRegistry.get(AssetManager.class).resolveAndTryLoadData(AssetType.BLOCK_TILE, name, TileData.class);
        if (tile != null) {
            tilesNormal.add(tile);
        } else {
            tilesNormal.add(defaultNormal);
        }
    }

    private void addHeightMap(AssetUri uri) {
        String name = uri.toSimpleString() + "Height";
        TileData tile = CoreRegistry.get(AssetManager.class).resolveAndTryLoadData(AssetType.BLOCK_TILE, name, TileData.class);
        if (tile != null) {
            tilesHeight.add(tile);
        } else {
            tilesHeight.add(defaultHeight);
        }
    }

    private void buildAtlas() {
        calculateAtlasSizes();

        int numMipMaps = getNumMipmaps();
        ByteBuffer[] data = createAtlasMipmaps(numMipMaps, TRANSPARENT_COLOR, tiles, "tiles.png");
        ByteBuffer[] dataNormal = createAtlasMipmaps(numMipMaps, UNIT_Z_COLOR, tilesNormal, "tilesNormal.png");
        ByteBuffer[] dataHeight = createAtlasMipmaps(numMipMaps, BLACK_COLOR, tilesHeight, "tilesHeight.png");

        TextureData terrainTexData = new TextureData(atlasSize, atlasSize, data, Texture.WrapMode.CLAMP, Texture.FilterMode.NEAREST);
        Texture terrainTex = Assets.generateAsset(new AssetUri(AssetType.TEXTURE, "engine:terrain"), terrainTexData, Texture.class);

        TextureData terrainNormalData = new TextureData(atlasSize, atlasSize, dataNormal, Texture.WrapMode.CLAMP, Texture.FilterMode.NEAREST);
        Assets.generateAsset(new AssetUri(AssetType.TEXTURE, "engine:terrainNormal"), terrainNormalData, Texture.class);

        TextureData terrainHeightData = new TextureData(atlasSize, atlasSize, dataHeight, Texture.WrapMode.CLAMP, Texture.FilterMode.NEAREST);
        Assets.generateAsset(new AssetUri(AssetType.TEXTURE, "engine:terrainHeight"), terrainHeightData, Texture.class);

        MaterialData terrainMatData = new MaterialData(Assets.getShader("engine:block"));
        terrainMatData.setParam("textureAtlas", terrainTex);
        terrainMatData.setParam("colorOffset", new float[]{1, 1, 1});
        terrainMatData.setParam("textured", true);
        Assets.generateAsset(new AssetUri(AssetType.MATERIAL, "engine:terrain"), terrainMatData, Material.class);

        createTextureAtlas(terrainTex);
    }

    private void createTextureAtlas(final Texture texture) {
        final Map<Name, Map<Name, SubtextureData>> textureAtlases = Maps.newHashMap();
        final Vector2f texSize = new Vector2f(getRelativeTileSize(), getRelativeTileSize());
        tileIndexes.forEachEntry(new TObjectIntProcedure<AssetUri>() {
            @Override
            public boolean execute(AssetUri tileUri, int index) {
                Vector2f coords = getTexCoords(index);
                SubtextureData subtextureData = new SubtextureData(texture, Rect2f.createFromMinAndSize(coords, texSize));

                Map<Name, SubtextureData> textureAtlas = textureAtlases.get(tileUri.getModuleName());
                if (textureAtlas == null) {
                    textureAtlas = Maps.newHashMap();
                    textureAtlases.put(tileUri.getModuleName(), textureAtlas);
                }
                textureAtlas.put(tileUri.getAssetName(), subtextureData);

                return true;
            }
        });

        for (Map.Entry<Name, Map<Name, SubtextureData>> atlas : textureAtlases.entrySet()) {
            AtlasData data = new AtlasData(atlas.getValue());
            Assets.generateAsset(new AssetUri(AssetType.ATLAS, atlas.getKey(), new Name("terrain")), data, Atlas.class);
        }
    }

    private ByteBuffer[] createAtlasMipmaps(int numMipMaps, Color initialColor, List<TileData> tileImages, String screenshotName) {
        ByteBuffer[] data = new ByteBuffer[numMipMaps];
        for (int i = 0; i < numMipMaps; ++i) {
            BufferedImage image = generateAtlas(i, tileImages, initialColor);
            if (i == 0) {
                try (OutputStream stream = new BufferedOutputStream(Files.newOutputStream(PathManager.getInstance().getScreenshotPath().resolve(screenshotName)))) {
                    ImageIO.write(image, "png", stream);
                } catch (IOException e) {
                    logger.warn("Failed to write atlas");
                }
            }

            try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
                ImageIO.write(image, "png", bos);
                PNGDecoder decoder = new PNGDecoder(new ByteArrayInputStream(bos.toByteArray()));
                ByteBuffer buf = ByteBuffer.allocateDirect(4 * decoder.getWidth() * decoder.getHeight());
                decoder.decode(buf, decoder.getWidth() * 4, PNGDecoder.RGBA);
                buf.flip();
                data[i] = buf;
            } catch (IOException e) {
                logger.error("Failed to create atlas texture");
            }
        }
        return data;
    }

    // The atlas is configured using the following constraints...
    // 1.   The overall tile size is the size of the largest tile loaded
    // 2.   The atlas will never be larger than 4096*4096 px
    // 3.   The tile size gets adjusted if the tiles won't fit into the atlas using the overall tile size
    //      (the tile size gets halved until all tiles will fit into the atlas)
    // 4.   The size of the atlas is always a power of two - as is the tile size
    private void calculateAtlasSizes() {
        tileSize = 16;
        for (TileData tile : tiles) {
            if (tile.getImage().getWidth() > tileSize) {
                tileSize = tile.getImage().getWidth();
            }
        }

        atlasSize = 1;
        while (atlasSize * atlasSize < tiles.size()) {
            atlasSize *= 2;
        }
        atlasSize = atlasSize * tileSize;

        if (atlasSize > maxAtlasSize) {
            atlasSize = maxAtlasSize;
            int maxTiles = (atlasSize / tileSize) * (atlasSize / tileSize);
            while (maxTiles < tiles.size()) {
                tileSize >>= 1;
                maxTiles = (atlasSize / tileSize) * (atlasSize / tileSize);
            }
        }
    }

    private BufferedImage generateAtlas(int mipMapLevel, List<TileData> tileImages, Color clearColor) {
        int size = atlasSize / (1 << mipMapLevel);
        int textureSize = tileSize / (1 << mipMapLevel);
        int tilesPerDim = atlasSize / tileSize;

        BufferedImage result = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
        Graphics g = result.getGraphics();

        g.setColor(clearColor);
        g.fillRect(0, 0, size, size);
        for (int index = 0; index < tileImages.size(); ++index) {
            int posX = (index) % tilesPerDim;
            int posY = (index) / tilesPerDim;
            TileData tile = tileImages.get(index);
            if (tile != null) {
                g.drawImage(tile.getImage().getScaledInstance(textureSize, textureSize, Image.SCALE_SMOOTH), posX * textureSize, posY * textureSize, null);
            }
        }

        return result;
    }
}
TOP

Related Classes of org.terasology.world.block.loader.WorldAtlasImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.