mirror of
https://github.com/BlueMap-Minecraft/BlueMap.git
synced 2025-02-03 14:11:21 +01:00
Fix black spots in lowres layer (syncronization issue)
This commit is contained in:
parent
0a4d85982c
commit
5448850eca
@ -0,0 +1,158 @@
|
|||||||
|
package de.bluecolored.bluemap.core.map.lowres;
|
||||||
|
|
||||||
|
import com.flowpowered.math.vector.Vector2i;
|
||||||
|
import com.github.benmanes.caffeine.cache.*;
|
||||||
|
import de.bluecolored.bluemap.core.BlueMap;
|
||||||
|
import de.bluecolored.bluemap.core.logger.Logger;
|
||||||
|
import de.bluecolored.bluemap.core.storage.Storage;
|
||||||
|
import de.bluecolored.bluemap.core.util.Vector2iCache;
|
||||||
|
import de.bluecolored.bluemap.core.util.math.Color;
|
||||||
|
import de.bluecolored.bluemap.core.world.Grid;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class LowresLayer {
|
||||||
|
|
||||||
|
private static final Vector2iCache VECTOR_2_I_CACHE = new Vector2iCache();
|
||||||
|
|
||||||
|
private final Storage.MapStorage mapStorage;
|
||||||
|
|
||||||
|
private final Grid tileGrid;
|
||||||
|
private final int lodFactor;
|
||||||
|
|
||||||
|
private final int lod;
|
||||||
|
private final LoadingCache<Vector2i, LowresTile> tileCache;
|
||||||
|
@Nullable private final LowresLayer nextLayer;
|
||||||
|
|
||||||
|
public LowresLayer(
|
||||||
|
Storage.MapStorage mapStorage, Grid tileGrid, int lodCount, int lodFactor,
|
||||||
|
int lod, @Nullable LowresLayer nextLayer
|
||||||
|
) {
|
||||||
|
this.mapStorage = mapStorage;
|
||||||
|
|
||||||
|
this.tileGrid = tileGrid;
|
||||||
|
this.lodFactor = lodFactor;
|
||||||
|
|
||||||
|
this.lod = lod;
|
||||||
|
this.nextLayer = nextLayer;
|
||||||
|
|
||||||
|
// this extra cache makes sure that a tile instance is reused as long as it is still referenced somewhere ..
|
||||||
|
// so always only one instance of the same lowres-tile exists
|
||||||
|
LoadingCache<Vector2i, LowresTile> tileWeakInstanceCache = Caffeine.newBuilder()
|
||||||
|
.executor(BlueMap.THREAD_POOL)
|
||||||
|
.weakValues()
|
||||||
|
.build(this::createTile);
|
||||||
|
|
||||||
|
this.tileCache = Caffeine.newBuilder()
|
||||||
|
.executor(BlueMap.THREAD_POOL)
|
||||||
|
.scheduler(Scheduler.systemScheduler())
|
||||||
|
.expireAfterAccess(10, TimeUnit.SECONDS)
|
||||||
|
.expireAfterWrite(5, TimeUnit.MINUTES)
|
||||||
|
.removalListener(this::saveTile)
|
||||||
|
.build(tileWeakInstanceCache::get);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
Logger.global.logInfo("Saving lowres layer " + lod);
|
||||||
|
tileCache.invalidateAll();
|
||||||
|
tileCache.cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LowresTile createTile(Vector2i tilePos) {
|
||||||
|
try (InputStream in = mapStorage.read(lod, tilePos).orElse(null)) {
|
||||||
|
if (in == null)
|
||||||
|
return new LowresTile(tileGrid.getGridSize());
|
||||||
|
return new LowresTile(tileGrid.getGridSize(), in);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.global.logError("Failed to load tile " + tilePos + " (lod: " + lod + ")", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveTile(Vector2i tilePos, @Nullable LowresTile tile, RemovalCause removalCause) {
|
||||||
|
if (tile == null) return;
|
||||||
|
|
||||||
|
// save the tile
|
||||||
|
try (OutputStream out = mapStorage.write(lod, tilePos)) {
|
||||||
|
tile.save(out);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.global.logError("Failed to save tile " + tilePos + " (lod: " + lod + ")", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write to next LOD (prepare for the most confusing grid-math you will ever see)
|
||||||
|
if (this.nextLayer == null) return;
|
||||||
|
|
||||||
|
Color averageColor = new Color();
|
||||||
|
int averageHeight, averageBlockLight;
|
||||||
|
int count;
|
||||||
|
|
||||||
|
Color color = new Color();
|
||||||
|
|
||||||
|
int nextLodTileX = Math.floorDiv(tilePos.getX(), lodFactor);
|
||||||
|
int nextLodTileY = Math.floorDiv(tilePos.getY(), lodFactor);
|
||||||
|
Vector2i groupCount = new Vector2i(
|
||||||
|
Math.floorDiv(tileGrid.getGridSize().getX(), lodFactor),
|
||||||
|
Math.floorDiv(tileGrid.getGridSize().getY(), lodFactor)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (int gX = 0; gX < groupCount.getX(); gX++) {
|
||||||
|
for (int gY = 0; gY < groupCount.getY(); gY++) {
|
||||||
|
averageColor.set(0, 0, 0, 0, true);
|
||||||
|
averageHeight = 0;
|
||||||
|
averageBlockLight = 0;
|
||||||
|
count = 0;
|
||||||
|
for (int x = 0; x < lodFactor; x++) {
|
||||||
|
for (int y = 0; y < lodFactor; y++) {
|
||||||
|
count++;
|
||||||
|
averageColor.add(tile.getColor(gX * lodFactor + x, gY * lodFactor + y, color).premultiplied());
|
||||||
|
averageHeight += tile.getHeight(gX * lodFactor + x, gY * lodFactor + y);
|
||||||
|
averageBlockLight += tile.getBlockLight(gX * lodFactor + x, gY * lodFactor + y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
averageColor.div(count);
|
||||||
|
averageHeight /= count;
|
||||||
|
averageBlockLight /= count;
|
||||||
|
|
||||||
|
this.nextLayer.set(
|
||||||
|
nextLodTileX,
|
||||||
|
nextLodTileY,
|
||||||
|
Math.floorMod(tilePos.getX(), lodFactor) * groupCount.getX() + gX,
|
||||||
|
Math.floorMod(tilePos.getY(), lodFactor) * groupCount.getY() + gY,
|
||||||
|
averageColor,
|
||||||
|
averageHeight,
|
||||||
|
averageBlockLight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LowresTile getTile(int x, int z) {
|
||||||
|
return tileCache.get(VECTOR_2_I_CACHE.get(x, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(int cellX, int cellZ, int pixelX, int pixelZ, Color color, int height, int blockLight) {
|
||||||
|
getTile(cellX, cellZ)
|
||||||
|
.set(pixelX, pixelZ, color, height, blockLight);
|
||||||
|
|
||||||
|
// for seamless edges
|
||||||
|
if (pixelX == 0) {
|
||||||
|
getTile(cellX - 1, cellZ)
|
||||||
|
.set(tileGrid.getGridSize().getX(), pixelZ, color, height, blockLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixelZ == 0) {
|
||||||
|
getTile(cellX, cellZ - 1)
|
||||||
|
.set(pixelX, tileGrid.getGridSize().getY(), color, height, blockLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixelX == 0 && pixelZ == 0) {
|
||||||
|
getTile(cellX - 1, cellZ - 1)
|
||||||
|
.set(tileGrid.getGridSize().getX(), tileGrid.getGridSize().getY(), color, height, blockLight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,16 +8,17 @@
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
public class LowresTile {
|
public class LowresTile {
|
||||||
|
|
||||||
public static final int HEIGHT_UNDEFINED = Integer.MIN_VALUE;
|
public static final int HEIGHT_UNDEFINED = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
private final BufferedImage texture;
|
private final BufferedImage texture;
|
||||||
private final Vector2i size;
|
private final Vector2i size;
|
||||||
|
|
||||||
private volatile boolean closed = false;
|
|
||||||
|
|
||||||
public LowresTile(Vector2i tileSize) {
|
public LowresTile(Vector2i tileSize) {
|
||||||
this.size = tileSize.add(1, 1); // add 1 for seamless edges
|
this.size = tileSize.add(1, 1); // add 1 for seamless edges
|
||||||
this.texture = new BufferedImage(this.size.getX(), this.size.getY() * 2, BufferedImage.TYPE_INT_ARGB);
|
this.texture = new BufferedImage(this.size.getX(), this.size.getY() * 2, BufferedImage.TYPE_INT_ARGB);
|
||||||
@ -32,14 +33,18 @@ public LowresTile(Vector2i tileSize, InputStream in) throws IOException {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set(int x, int z, Color color, int height, int blockLight) throws TileClosedException {
|
public void set(int x, int z, Color color, int height, int blockLight) {
|
||||||
if (closed) throw new TileClosedException();
|
lock.readLock().lock();
|
||||||
|
try {
|
||||||
texture.setRGB(x, z, color.straight().getInt());
|
texture.setRGB(x, z, color.straight().getInt());
|
||||||
texture.setRGB(x, size.getY() + z,
|
texture.setRGB(x, size.getY() + z,
|
||||||
(height & 0x0000FFFF) |
|
(height & 0x0000FFFF) |
|
||||||
((blockLight << 16) & 0x00FF0000) |
|
((blockLight << 16) & 0x00FF0000) |
|
||||||
0xFF000000
|
0xFF000000
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
lock.readLock().unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color getColor(int x, int z, Color target) {
|
public Color getColor(int x, int z, Color target) {
|
||||||
@ -58,16 +63,11 @@ public int getBlockLight(int x, int z) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void save(OutputStream out) throws IOException {
|
public void save(OutputStream out) throws IOException {
|
||||||
|
lock.writeLock().lock();
|
||||||
|
try {
|
||||||
ImageIO.write(texture, "png", out);
|
ImageIO.write(texture, "png", out);
|
||||||
}
|
} finally {
|
||||||
|
lock.writeLock().unlock();
|
||||||
public void close() {
|
|
||||||
closed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class TileClosedException extends Exception {
|
|
||||||
public TileClosedException() {
|
|
||||||
super("Tile is closed");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,193 +1,35 @@
|
|||||||
package de.bluecolored.bluemap.core.map.lowres;
|
package de.bluecolored.bluemap.core.map.lowres;
|
||||||
|
|
||||||
import com.flowpowered.math.vector.Vector2i;
|
|
||||||
import com.github.benmanes.caffeine.cache.*;
|
|
||||||
import de.bluecolored.bluemap.core.BlueMap;
|
|
||||||
import de.bluecolored.bluemap.core.logger.Logger;
|
|
||||||
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
|
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
|
||||||
import de.bluecolored.bluemap.core.storage.Storage;
|
import de.bluecolored.bluemap.core.storage.Storage;
|
||||||
import de.bluecolored.bluemap.core.util.Vector2iCache;
|
|
||||||
import de.bluecolored.bluemap.core.util.math.Color;
|
import de.bluecolored.bluemap.core.util.math.Color;
|
||||||
import de.bluecolored.bluemap.core.world.Grid;
|
import de.bluecolored.bluemap.core.world.Grid;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class LowresTileManager implements TileMetaConsumer {
|
public class LowresTileManager implements TileMetaConsumer {
|
||||||
|
|
||||||
private final Storage.MapStorage mapStorage;
|
|
||||||
|
|
||||||
private final Grid tileGrid;
|
private final Grid tileGrid;
|
||||||
private final int lodFactor, lodCount;
|
private final int lodFactor, lodCount;
|
||||||
|
|
||||||
private final Vector2iCache vector2iCache;
|
private final LowresLayer[] layers;
|
||||||
private final List<LoadingCache<Vector2i, LowresTile>> tileCaches;
|
|
||||||
|
|
||||||
public LowresTileManager(Storage.MapStorage mapStorage, Grid tileGrid, int lodCount, int lodFactor) {
|
public LowresTileManager(Storage.MapStorage mapStorage, Grid tileGrid, int lodCount, int lodFactor) {
|
||||||
this.mapStorage = mapStorage;
|
|
||||||
|
|
||||||
this.tileGrid = tileGrid;
|
this.tileGrid = tileGrid;
|
||||||
this.lodFactor = lodFactor;
|
this.lodFactor = lodFactor;
|
||||||
this.lodCount = lodCount;
|
this.lodCount = lodCount;
|
||||||
|
|
||||||
this.vector2iCache = new Vector2iCache();
|
this.layers = new LowresLayer[lodCount];
|
||||||
List<LoadingCache<Vector2i, LowresTile>> tileCacheList = new ArrayList<>();
|
for (int i = lodCount - 1; i >= 0; i--) {
|
||||||
for (int i = 0; i < lodCount; i++) {
|
this.layers[i] = new LowresLayer(mapStorage, tileGrid, lodCount, lodFactor, i + 1,
|
||||||
int lod = i + 1;
|
(i == lodCount - 1) ? null : layers[i + 1]);
|
||||||
tileCacheList.add(Caffeine.newBuilder()
|
|
||||||
.executor(BlueMap.THREAD_POOL)
|
|
||||||
.scheduler(Scheduler.systemScheduler())
|
|
||||||
.expireAfterAccess(10, TimeUnit.SECONDS)
|
|
||||||
.expireAfterWrite(5, TimeUnit.MINUTES)
|
|
||||||
.writer(new CacheWriter<Vector2i, LowresTile>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(@NonNull Vector2i key, @NonNull LowresTile value) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(@NonNull Vector2i key, @Nullable LowresTile value, @NonNull RemovalCause cause) {
|
|
||||||
if (value != null)
|
|
||||||
saveTile(key, lod, value, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.build(key -> loadTile(key, lod))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.tileCaches = Collections.unmodifiableList(tileCacheList);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LowresTile loadTile(Vector2i tilePos, int lod) {
|
|
||||||
try (InputStream in = mapStorage.read(lod, tilePos).orElse(null)) {
|
|
||||||
if (in == null)
|
|
||||||
return new LowresTile(tileGrid.getGridSize());
|
|
||||||
return new LowresTile(tileGrid.getGridSize(), in);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Logger.global.logError("Failed to load tile " + tilePos + " (lod: " + lod + ")", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private void saveTile(Vector2i tilePos, int lod, LowresTile tile, RemovalCause removalCause) {
|
|
||||||
// close the tile so it can't be edited anymore
|
|
||||||
tile.close();
|
|
||||||
|
|
||||||
// save the tile
|
|
||||||
try (OutputStream out = mapStorage.write(lod, tilePos)) {
|
|
||||||
tile.save(out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Logger.global.logError("Failed to save tile " + tilePos + " (lod: " + lod + ")", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lod >= lodCount) return;
|
|
||||||
|
|
||||||
// write to next LOD (prepare for the most confusing grid-math you will ever see)
|
|
||||||
Color averageColor = new Color();
|
|
||||||
int averageHeight, averageBlockLight;
|
|
||||||
int count;
|
|
||||||
|
|
||||||
Color color = new Color();
|
|
||||||
|
|
||||||
int nextLodTileX = Math.floorDiv(tilePos.getX(), lodFactor);
|
|
||||||
int nextLodTileY = Math.floorDiv(tilePos.getY(), lodFactor);
|
|
||||||
Vector2i groupCount = new Vector2i(
|
|
||||||
Math.floorDiv(tileGrid.getGridSize().getX(), lodFactor),
|
|
||||||
Math.floorDiv(tileGrid.getGridSize().getY(), lodFactor)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (int gX = 0; gX < groupCount.getX(); gX++) {
|
|
||||||
for (int gY = 0; gY < groupCount.getY(); gY++) {
|
|
||||||
averageColor.set(0, 0, 0, 0, true);
|
|
||||||
averageHeight = 0;
|
|
||||||
averageBlockLight = 0;
|
|
||||||
count = 0;
|
|
||||||
for (int x = 0; x < lodFactor; x++) {
|
|
||||||
for (int y = 0; y < lodFactor; y++) {
|
|
||||||
count++;
|
|
||||||
averageColor.add(tile.getColor(gX * lodFactor + x, gY * lodFactor + y, color).premultiplied());
|
|
||||||
averageHeight += tile.getHeight(gX * lodFactor + x, gY * lodFactor + y);
|
|
||||||
averageBlockLight += tile.getBlockLight(gX * lodFactor + x, gY * lodFactor + y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
averageColor.div(count);
|
|
||||||
averageHeight /= count;
|
|
||||||
averageBlockLight /= count;
|
|
||||||
|
|
||||||
set(
|
|
||||||
nextLodTileX,
|
|
||||||
nextLodTileY,
|
|
||||||
lod + 1,
|
|
||||||
Math.floorMod(tilePos.getX(), lodFactor) * groupCount.getX() + gX,
|
|
||||||
Math.floorMod(tilePos.getY(), lodFactor) * groupCount.getY() + gY,
|
|
||||||
averageColor,
|
|
||||||
averageHeight,
|
|
||||||
averageBlockLight
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void save() {
|
public synchronized void save() {
|
||||||
for (LoadingCache<Vector2i, LowresTile> cache : this.tileCaches) {
|
for (LowresLayer layer : this.layers) {
|
||||||
cache.invalidateAll();
|
layer.save();
|
||||||
cache.cleanUp();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LowresTile getTile(int x, int z, int lod) {
|
|
||||||
return tileCaches.get(lod - 1).get(vector2iCache.get(x, z));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void set(int x, int z, Color color, int height, int blockLight) {
|
|
||||||
int cellX = tileGrid.getCellX(x);
|
|
||||||
int cellZ = tileGrid.getCellY(z);
|
|
||||||
int localX = tileGrid.getLocalX(x);
|
|
||||||
int localZ = tileGrid.getLocalY(z);
|
|
||||||
set(cellX, cellZ, 1, localX, localZ, color, height, blockLight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void set(int cellX, int cellZ, int lod, int pixelX, int pixelZ, Color color, int height, int blockLight) {
|
|
||||||
|
|
||||||
int tries = 0;
|
|
||||||
LowresTile.TileClosedException closedException;
|
|
||||||
do {
|
|
||||||
tries ++;
|
|
||||||
closedException = null;
|
|
||||||
try {
|
|
||||||
getTile(cellX, cellZ, lod)
|
|
||||||
.set(pixelX, pixelZ, color, height, blockLight);
|
|
||||||
|
|
||||||
// for seamless edges
|
|
||||||
if (pixelX == 0) {
|
|
||||||
getTile(cellX - 1, cellZ, lod)
|
|
||||||
.set(tileGrid.getGridSize().getX(), pixelZ, color, height, blockLight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pixelZ == 0) {
|
|
||||||
getTile(cellX, cellZ - 1, lod)
|
|
||||||
.set(pixelX, tileGrid.getGridSize().getY(), color, height, blockLight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pixelX == 0 && pixelZ == 0) {
|
|
||||||
getTile(cellX - 1, cellZ - 1, lod)
|
|
||||||
.set(tileGrid.getGridSize().getX(), tileGrid.getGridSize().getY(), color, height, blockLight);
|
|
||||||
}
|
|
||||||
} catch (LowresTile.TileClosedException ex) {
|
|
||||||
closedException = ex;
|
|
||||||
}
|
|
||||||
} while (closedException != null && tries < 10);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public Grid getTileGrid() {
|
public Grid getTileGrid() {
|
||||||
return tileGrid;
|
return tileGrid;
|
||||||
}
|
}
|
||||||
@ -200,4 +42,13 @@ public int getLodFactor() {
|
|||||||
return lodFactor;
|
return lodFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(int x, int z, Color color, int height, int blockLight) {
|
||||||
|
int cellX = tileGrid.getCellX(x);
|
||||||
|
int cellZ = tileGrid.getCellY(z);
|
||||||
|
int localX = tileGrid.getLocalX(x);
|
||||||
|
int localZ = tileGrid.getLocalY(z);
|
||||||
|
layers[0].set(cellX, cellZ, localX, localZ, color, height, blockLight);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user