/* Copyright (c) 2014 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.map;
import org.apache.commons.math3.util.FastMath;
import se.llbit.chunky.resources.Texture;
import se.llbit.chunky.world.Biomes;
import se.llbit.chunky.world.Block;
import se.llbit.chunky.world.Chunk;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.Heightmap;
import se.llbit.chunky.world.World;
import se.llbit.math.ColorUtil;
import se.llbit.math.QuickMath;
import java.io.IOException;
import java.io.OutputStream;
public class SurfaceLayer extends BitmapLayer {
private final int[] bitmap;
private final int[] topo;
private int avgColor = 0xFF;
/**
* Generate the surface bitmap.
*
* @param dim current dimension
* @param blocksArray block id array
*/
public SurfaceLayer(int dim, byte[] blocksArray, byte[] biomes, byte[] blockData) {
bitmap = new int[Chunk.X_MAX * Chunk.Z_MAX];
topo = new int[Chunk.X_MAX * Chunk.Z_MAX];
for (int x = 0; x < Chunk.X_MAX; ++x) {
for (int z = 0; z < Chunk.Z_MAX; ++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];
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)];
int data = 0xFF & blockData[Chunk.chunkIndex(x, y, z) / 2];
data >>= (x % 2) * 4;
data &= 0xF;
switch (block.id) {
case Block.LEAVES_ID:
case Block.LEAVES2_ID:
ColorUtil.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:
ColorUtil.getRGBComponents(Biomes.getGrassColor(biomeId), blockColor);
blockColor[3] = 1.f;// grass colors don't include alpha
y -= 1;
break;
case Block.ICE_ID:
ColorUtil.getRGBAComponents(block.getTexture(data).getAvgColor(), blockColor);
color = blend(color, blockColor);
y -= 1;
for (; y >= 0; --y) {
if (Block.get(blocksArray[Chunk.chunkIndex(x, y, z)]).isOpaque) {
ColorUtil.getRGBAComponents(block.getTexture(data).getAvgColor(), 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;
}
ColorUtil.getRGBAComponents(Texture.water.getAvgColor(), blockColor);
blockColor[3] = QuickMath.max(.5f, 1.f - depth / 32.f);
break;
default:
ColorUtil.getRGBAComponents(block.getTexture(data).getAvgColor(), 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;
}
}
bitmap[x * 16 + z] = ColorUtil.getArgb(color[0], color[1], color[2], color[3]);
topo[x * 16 + z] = bitmap[x * 16 + z];
}
}
avgColor = avgBitmapColor();
}
/**
* Add topographical gradient to this chunk and calculate average color
*/
@Override 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) {
ColorUtil.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]);
topo[x * 16 + z] = ColorUtil.getRGB(rgb[0], rgb[1], rgb[2]);
}
}
}
/**
* Blend the two argb colors a and b. Result is stored in the array a.
*/
private static 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;
}
private int avgBitmapColor() {
float[] avg = new float[3];
float[] frgb = new float[3];
for (int i = 0; i < 16 * 16; ++i) {
ColorUtil.getRGBComponents(bitmap[i], frgb);
avg[0] += frgb[0];
avg[1] += frgb[1];
avg[2] += frgb[2];
}
return ColorUtil.getRGB(avg[0] / (16 * 16), avg[1] / (16 * 16), avg[2] / (16 * 16));
}
/**
* Write a PNG scanline.
*
* @throws IOException
*/
@Override 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 {
super.writePngLine(scanline, out);
}
}
/**
* @return The average color of this layer
*/
@Override public int getAvgColor() {
return avgColor;
}
@Override public int colorAt(int x, int z) {
return topo[x * 16 + z];
}
}