diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 3da8435..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,8 +0,0 @@
-[submodule "vendor/chunky"]
- path = vendor/chunky
- url = https://github.com/leMaik/chunky
-[submodule "."]
- branch = remove-static-registries
-[submodule "vendor/chunky-denoiser"]
- path = vendor/chunky-denoiser
- url = https://github.com/chunky-dev/chunky-denoiser
diff --git a/README.md b/README.md
index c20f962..43ee52a 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,14 @@ ChunkyMap is a map renderer for [Dynmap][dynmap] that uses [Chunky][chunky] to r

+## Compatibility matrix
+
+| ChunkyMap | Minecraft | Dynmap | Chunky | Branch |
+| ---------- | --------------- | ------------ | ------ | ---------- |
+| 2.7.0-pre1 | 1.21.4 or older | 3.7 | 2.5.0 | chunky-2.5 |
+| 2.6.0-pre4 | 1.19 or older | 3.2 or later | 2.4.4 | chunky-2.4 |
+| 2.5.2 | 1.16 or older | 2.3-3.0 | 2.3.0 | master |
+
## Installation
1. Download the latest jar from [the releases page][latest-release] and put it in your plugins directory.
@@ -32,18 +40,25 @@ ChunkyMap is a map renderer for [Dynmap][dynmap] that uses [Chunky][chunky] to r
The maps can be configured by adding options to the map's section in the `world.txt` file.
-| Option | Description | Default |
-| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
-| `samplesPerPixel` | Samples per pixel that Chunky should render. More SPP improves the render quality but also increases render time. | 100 |
-| `chunkyThreads` | Number of threads per Chunky instance. More threads will decrease render time but increase the CPU load of your server. | 2 |
-| `chunkyCpuLoad` | Percentage of CPU time to use, per Chunky thread. Note that this only throttles the CPU usage during rendering, not during scene loading or post processing. | 100 |
-| `texturepack` | Texturepack path, relative to `plugins/dynmap`. Use this option to specify a texturepack for a map. The texturepack in Dynmap's `configuration.txt` is ignored by ChunkyMap. | _None_ |
-| `chunkPadding` | Radius of additional chunks to be loaded around each chunk that is required to render a tile of the map. This can be used to reduce artifacts caused by shadows and reflections. | 0 |
-| `templateScene` | Path to a Chunky scene file (JSON), relative to `plugins/dynmap`. Use this option to customize the scene that is used for rendering the tiles, e.g. to change the water color. | _None_ |
-| `texturepackVersion` | The Minecraft version that should be used as fallback textures | 1.16.2 |
-| `denoiser.enabled` | Enable denoising using [Intel Open Image Denoise](https://openimagedenoise.github.io/). Only works on Linux | false |
-| `denoiser.albedoSamplesPerPixel` | Samples per pixel for the albedo map. Setting this to 0 will disable the albedo and normal map. | 4 |
-| `denoiser.normalSamplesPerPixel` | Samples per pixel for the normal map. Setting this to 0 will disable the normal map. | 4 |
+| Option | Description | Default |
+| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
+| `samplesPerPixel` | Samples per pixel that Chunky should render. More SPP improves the render quality but also increases render time. | 100 |
+| `chunkyThreads` | Number of threads per Chunky instance. More threads will decrease render time but increase the CPU load of your server. | 2 |
+| `chunkyCpuLoad` | Percentage of CPU time to use, per Chunky thread. Note that this only throttles the CPU usage during rendering, not during scene loading or post processing. | 100 |
+| ~~`texturepack`~~ (deprecated) | Texturepack path, relative to `plugins/dynmap`. Use this option to specify a texturepack for a map. The texturepack in Dynmap's `configuration.txt` is ignored by ChunkyMap. | _None_ |
+| `resourcepacks` | List of resourcepack paths, relative to `plugins/dynmap`. This also supports data packs for custom biomes. Use this option to specify resourcepacks for a map. The texturepack in Dynmap's `configuration.txt` is ignored by ChunkyMap. | _None_ |
+| `chunkPadding` | Radius of additional chunks to be loaded around each chunk that is required to render a tile of the map. This can be used to reduce artifacts caused by shadows and reflections. | 0 |
+| `requeueFailedTiles` | Put tiles that failed to render back into the tile queue. | true |
+| `templateScene` | Path to a Chunky scene file (JSON), relative to `plugins/dynmap`. Use this option to customize the scene that is used for rendering the tiles, e.g. to change the water color. | _None_ |
+| `texturepackVersion` | The Minecraft version that should be used as fallback textures | 1.21.4 |
+| `denoiser/enabled` | Enable denoising using [Intel Open Image Denoise](https://openimagedenoise.github.io/). Only works on Linux | false |
+| `denoiser/albedoSamplesPerPixel` | Samples per pixel for the albedo map. Setting this to 0 will disable the albedo and normal map. | 4 |
+| `denoiser/normalSamplesPerPixel` | Samples per pixel for the normal map. Setting this to 0 will disable the normal map. | 4 |
+| `chunkycloud/enabled` | Render tiles using the Chunky Cloud render service | false |
+| `chunkycloud/apiKey` | API Key for the Chunky Cloud render service | |
+| `chunkycloud/initializeLocally` | Generate the octree locally. Less data to upload, faster render times but will use a lot of CPU locally. | true |
+
+:warning: A forward slash (`/`) in the option name means that the right part is a nested option and needs to be put into the next line and indented properly. Take a look at the examples below.
## Example configurations
@@ -99,6 +114,25 @@ perspectives:
maximumheight: 100 # the bedrock layer is at 127
```
+### Rendering ChunkyMap on ChunkyCloud
+
+`plugins/dynmap/worlds.txt`:
+
+```yml
+worlds:
+ - name: world
+ maps:
+ - class: de.lemaik.chunkymap.dynmap.ChunkyMap
+ name: chunky
+ title: Chunky
+ perspective: iso_SE_30_hires
+ samplesPerPixel: 20
+ chunkycloud:
+ enabled: true
+ initializeLocally: false
+ apiKey: your-secret-api-key
+```
+
### Customizing the look of a map with template scenes
You can change how the map looks by providing a template scene. That can be any Chunky scene (`.json`) file or a partial scene file (i.e. a `.json` file that only contains the values that should be changed). ChunkyMap will import many scene options from the template scene, including the sun position, fog and water configuration.
diff --git a/banner.png b/banner.png
index 6356abe..e8f287c 100644
Binary files a/banner.png and b/banner.png differ
diff --git a/pom.xml b/pom.xml
index 7715804..fc4cdab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,12 +1,10 @@
-
+
4.0.0
de.lemaik.chunkymap
ChunkyMap
- 2.5.2
+ 2.7.0-pre1
@@ -16,10 +14,9 @@
- 1.8
+ 21
UTF-8
UTF-8
- 2.3.0-30-g82f6ab17
@@ -31,31 +28,23 @@
spigot-repo
https://hub.spigotmc.org/nexus/content/groups/public/
-
- dynmap-repo
- http://repo.mikeprimm.com/
-
wertarbyte-repo
https://repo.wertarbyte.com
-
- local-libs
- file://${basedir}/lib
-
- org.bukkit
- bukkit
- 1.16.1-R0.1-SNAPSHOT
+ org.spigotmc
+ spigot-api
+ 1.19.2-R0.1-SNAPSHOT
provided
se.llbit
chunky-core
- ${chunky.version}
+ 2.5.0-DEV.381.gf277ba6
@@ -64,26 +53,38 @@
3.2
-
us.dynmap
DynmapCore
- 2.3
+ 3.7-beta-8
+ provided
+
+
+
+ javax.servlet
+ javax.servlet-api
+
+
+
+
+ us.dynmap
+ DynmapCoreAPI
+ 3.7-beta-8
provided
com.squareup.okhttp3
okhttp
- 3.4.1
+ 3.14.9
com.google.code.gson
gson
- 2.7
+ 2.8.9
net.time4tea
oidnjni
- 0.1.04
+ 0.1.13
@@ -94,7 +95,7 @@
de.lemaik
chunky-denoiser
- 0.1.3-SNAPSHOT
+ 0.5.0-pre3
@@ -103,10 +104,10 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.5.1
+ 3.13.0
- 1.8
- 1.8
+ 21
+ 21
@@ -127,45 +128,6 @@
-
- org.apache.maven.plugins
- maven-install-plugin
- 2.5.2
-
-
- install-chunky
- clean
-
- ${basedir}/vendor/chunky/build/chunky-core-${chunky.version}.jar
- default
- se.llbit
- chunky-core
- ${chunky.version}
- jar
- true
-
-
- install-file
-
-
-
- install-chunky-denoiser
- clean
-
- ${basedir}/vendor/chunky-denoiser/build/libs/chunky-denoiser-chunky2.jar
- default
- de.lemaik
- chunky-denoiser
- 0.1.3-SNAPSHOT
- jar
- true
-
-
- install-file
-
-
-
-
@@ -174,4 +136,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java b/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java
index 3e0815f..0df0433 100644
--- a/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java
+++ b/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java
@@ -19,6 +19,7 @@
package de.lemaik.chunkymap;
+import de.lemaik.chunkymap.rendering.local.ChunkyLogAdapter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -27,12 +28,19 @@
import java.util.logging.Level;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
+import se.llbit.log.Log;
/**
* The main class.
*/
public class ChunkyMapPlugin extends JavaPlugin {
+ @Override
+ public void onLoad() {
+ Log.setReceiver(new ChunkyLogAdapter(getLogger()), se.llbit.log.Level.ERROR,
+ se.llbit.log.Level.WARNING, se.llbit.log.Level.INFO);
+ }
+
@Override
public void onEnable() {
Plugin dynmap = getServer().getPluginManager().getPlugin("dynmap");
diff --git a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java
index 4219f72..9addd59 100644
--- a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java
+++ b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java
@@ -3,29 +3,31 @@
import de.lemaik.chunkymap.ChunkyMapPlugin;
import de.lemaik.chunkymap.rendering.Renderer;
import de.lemaik.chunkymap.rendering.local.ChunkyRenderer;
+import de.lemaik.chunkymap.rendering.rs.RemoteRenderer;
import de.lemaik.chunkymap.util.MinecraftDownloader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import okhttp3.Response;
+import okhttp3.ResponseBody;
import okio.BufferedSink;
import okio.Okio;
import org.bukkit.Bukkit;
import org.dynmap.ConfigurationNode;
-import org.dynmap.DynmapChunk;
import org.dynmap.DynmapCore;
import org.dynmap.DynmapWorld;
import org.dynmap.MapTile;
import org.dynmap.MapType;
import org.dynmap.hdmap.HDMap;
+import org.dynmap.hdmap.HDPerspective;
import org.dynmap.hdmap.IsoHDPerspective;
-import org.dynmap.utils.TileFlags;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.json.JsonNumber;
import se.llbit.json.JsonObject;
@@ -36,38 +38,72 @@
*/
public class ChunkyMap extends HDMap {
- private static final String DEFAULT_TEXTUREPACK_VERSION = "1.16.2";
+ private static final String DEFAULT_TEXTUREPACK_VERSION;
public final DynmapCameraAdapter cameraAdapter;
private final Renderer renderer;
private File defaultTexturepackPath;
- private File texturepackPath;
+ private File[] resourcepackPaths;
+ private File worldPath;
+ private final Object worldPathLock = new Object();
private JsonObject templateScene;
- private int chunkPadding;
+ private final int chunkPadding;
+ private final boolean requeueFailedTiles;
+
+ static {
+ String bukkitVersion = Bukkit.getServer().getVersion();
+ String texturePackVersion = "1.21.4";
+ int start = bukkitVersion.indexOf("(MC:");
+ if (start >= 0) {
+ String minecraftVersion = bukkitVersion.substring(start + 5);
+ start = minecraftVersion.indexOf(")");
+ if(start >= 0) {
+ minecraftVersion = minecraftVersion.substring(0, start).trim();
+ texturePackVersion = minecraftVersion;
+ }
+ }
+ DEFAULT_TEXTUREPACK_VERSION = texturePackVersion;
+ }
public ChunkyMap(DynmapCore dynmap, ConfigurationNode config) {
super(dynmap, config);
cameraAdapter = new DynmapCameraAdapter((IsoHDPerspective) getPerspective());
- renderer = new ChunkyRenderer(
- config.getInteger("samplesPerPixel", 100),
- config.getBoolean("denoiser/enabled", false),
- config.getInteger("denoiser/albedoSamplesPerPixel", 16),
- config.getInteger("denoiser/normalSamplesPerPixel", 16),
- config.getInteger("chunkyThreads", 2),
- Math.min(100, Math.max(0, config.getInteger("chunkyCpuLoad", 100)))
- );
+ if (config.getBoolean("chunkycloud/enabled", false)) {
+ renderer = new RemoteRenderer(config.getString("chunkycloud/apiKey", ""),
+ config.getInteger("samplesPerPixel", 100),
+ config.getString("texturepack", null),
+ config.getBoolean("chunkycloud/initializeLocally", true));
+ if (config.getString("chunkycloud/apiKey", "").isEmpty()) {
+ ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger()
+ .warning("No ChunkyCloud API Key configured.");
+ }
+ } else {
+ renderer = new ChunkyRenderer(
+ config.getInteger("samplesPerPixel", 100),
+ config.getBoolean("denoiser/enabled", false),
+ config.getInteger("denoiser/albedoSamplesPerPixel", 16),
+ config.getInteger("denoiser/normalSamplesPerPixel", 16),
+ config.getInteger("chunkyThreads", 2),
+ Math.min(100, Math.max(0, config.getInteger("chunkyCpuLoad", 100)))
+ );
+ }
chunkPadding = config.getInteger("chunkPadding", 0);
+ requeueFailedTiles = config.getBoolean("requeueFailedTiles", true);
String texturepackVersion = config.getString("texturepackVersion", DEFAULT_TEXTUREPACK_VERSION);
File texturepackPath = new File(
ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getDataFolder(),
texturepackVersion + ".jar");
- if (!texturepackPath.exists()) {
+ if (texturepackPath.exists()) {
+ defaultTexturepackPath = texturepackPath;
+ } else {
ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger()
.info("Downloading additional textures for Minecraft " + texturepackVersion);
- try (Response response = MinecraftDownloader.downloadMinecraft(texturepackVersion).get()) {
- try (BufferedSink sink = Okio.buffer(Okio.sink(texturepackPath))) {
- sink.writeAll(response.body().source());
- }
+ try (
+ Response response = MinecraftDownloader.downloadMinecraft(texturepackVersion).get();
+ ResponseBody body = response.body();
+ BufferedSink sink = Okio.buffer(Okio.sink(texturepackPath))
+ ) {
+ sink.writeAll(body.source());
defaultTexturepackPath = texturepackPath;
} catch (IOException | ExecutionException | InterruptedException e) {
ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger()
@@ -76,78 +112,72 @@ public ChunkyMap(DynmapCore dynmap, ConfigurationNode config) {
}
}
- if (config.containsKey("texturepack")) {
- texturepackPath = Bukkit.getPluginManager().getPlugin("dynmap").getDataFolder().toPath()
- .resolve(config.getString("texturepack"))
- .toFile();
+ Path dynmapDataPath = Bukkit.getPluginManager().getPlugin("dynmap").getDataFolder().toPath();
+ if (config.containsKey("resourcepacks")) {
+ this.resourcepackPaths = config.getList("resourcepacks").stream()
+ .map(path -> dynmapDataPath.resolve(path.toString()).toFile()).toArray(File[]::new);
+ } else if (config.containsKey("texturepack")) {
+ this.resourcepackPaths = new File[] { dynmapDataPath
+ .resolve(config.getString("texturepack"))
+ .toFile() };
} else {
+ this.resourcepackPaths = new File[0];
ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger()
.warning("You didn't specify a texturepack for a map that is rendered with Chunky. " +
"The Minecraft " + texturepackVersion + " textures will be used.");
}
if (config.containsKey("templateScene")) {
- try (InputStream inputStream = new FileInputStream(
+ try (JsonParser parser = new JsonParser(new FileInputStream(
Bukkit.getPluginManager().getPlugin("dynmap").getDataFolder().toPath()
.resolve(config.getString("templateScene"))
- .toFile())) {
- templateScene = new JsonParser(inputStream).parse().asObject();
+ .toFile()))) {
+ templateScene = parser.parse().asObject();
templateScene.remove("world");
templateScene.set("spp", new JsonNumber(0));
templateScene.set("renderTime", new JsonNumber(0));
templateScene.remove("chunkList");
templateScene.remove("entities");
templateScene.remove("actors");
+ templateScene.remove("yClipMin");
+ templateScene.remove("yMin");
+ templateScene.remove("yClipMax");
+ templateScene.remove("yMax");
+ templateScene.remove("fullWidth");
+ templateScene.remove("fullHeight");
} catch (IOException | JsonParser.SyntaxError e) {
ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger()
.log(Level.SEVERE, "Could not read the template scene.", e);
}
}
-
- // texturepacks in chunky are static, so only load them once
- if (defaultTexturepackPath != null) {
- ChunkyRenderer.loadDefaultTexturepack(defaultTexturepackPath);
- }
- if (texturepackPath != null) {
- ChunkyRenderer.loadTexturepack(texturepackPath);
- }
}
@Override
public void addMapTiles(List list, DynmapWorld world, int tx, int ty) {
- list.add(new ChunkyMapTile(world, getPerspective(), this, tx, ty));
- }
-
- public List getTileCoords(DynmapWorld world, int x, int y, int z) {
- return getPerspective().getTileCoords(world, x, y, z);
- }
-
- public List getTileCoords(DynmapWorld world, int minx, int miny, int minz,
- int maxx, int maxy, int maxz) {
- return getPerspective().getTileCoords(world, minx, miny, minz, maxx, maxy, maxz);
+ MapTile tile = new ChunkyMapTile(world, getPerspective(), tx, ty, getBoostZoom(), getTileScale());
+ list.add(tile);
}
@Override
public MapTile[] getAdjecentTiles(MapTile tile) {
+ return getAdjecentTilesOfTile(tile, getPerspective());
+ }
+
+ public static MapTile[] getAdjecentTilesOfTile(MapTile tile, HDPerspective perspective) {
ChunkyMapTile t = (ChunkyMapTile) tile;
DynmapWorld w = t.getDynmapWorld();
int x = t.tileOrdinalX();
int y = t.tileOrdinalY();
return new MapTile[]{
- new ChunkyMapTile(w, getPerspective(), this, x - 1, y - 1),
- new ChunkyMapTile(w, getPerspective(), this, x - 1, y + 1),
- new ChunkyMapTile(w, getPerspective(), this, x + 1, y - 1),
- new ChunkyMapTile(w, getPerspective(), this, x + 1, y + 1),
- new ChunkyMapTile(w, getPerspective(), this, x, y - 1),
- new ChunkyMapTile(w, getPerspective(), this, x + 1, y),
- new ChunkyMapTile(w, getPerspective(), this, x, y + 1),
- new ChunkyMapTile(w, getPerspective(), this, x - 1, y)};
- }
-
- @Override
- public List getRequiredChunks(MapTile mapTile) {
- return getPerspective().getRequiredChunks(mapTile);
+ new ChunkyMapTile(w, perspective, x - 1, y - 1, t.boostzoom, t.tilescale),
+ new ChunkyMapTile(w, perspective, x - 1, y + 1, t.boostzoom, t.tilescale),
+ new ChunkyMapTile(w, perspective, x + 1, y - 1, t.boostzoom, t.tilescale),
+ new ChunkyMapTile(w, perspective, x + 1, y + 1, t.boostzoom, t.tilescale),
+ new ChunkyMapTile(w, perspective, x, y - 1, t.boostzoom, t.tilescale),
+ new ChunkyMapTile(w, perspective, x + 1, y, t.boostzoom, t.tilescale),
+ new ChunkyMapTile(w, perspective, x, y + 1, t.boostzoom, t.tilescale),
+ new ChunkyMapTile(w, perspective, x - 1, y, t.boostzoom, t.tilescale)};
}
@Override
@@ -179,17 +209,37 @@ File getDefaultTexturepackPath() {
return defaultTexturepackPath;
}
- File getTexturepackPath() {
- return texturepackPath;
+ File[] getResourcepackPaths() {
+ return resourcepackPaths;
}
int getChunkPadding() {
return chunkPadding;
}
+ public boolean getRequeueFailedTiles() {
+ return requeueFailedTiles;
+ }
+
void applyTemplateScene(Scene scene) {
if (this.templateScene != null) {
scene.importFromJson(templateScene);
}
}
+
+ File getWorldFolder(DynmapWorld world) {
+ if (worldPath == null) {
+ // Fixes a ConcurrentModificationException, see https://github.com/leMaik/ChunkyMap/issues/30
+ synchronized (worldPathLock) {
+ worldPath = Bukkit.getWorld(world.getRawName()).getWorldFolder();
+ }
+ }
+ return worldPath;
+ }
+
+ private static final ImageVariant[] variants = new ImageVariant[]{ImageVariant.STANDARD};
+ @Override
+ public ImageVariant[] getVariants() {
+ return variants;
+ }
}
diff --git a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java
index 60834b2..3f311a4 100644
--- a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java
+++ b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java
@@ -4,20 +4,18 @@
import de.lemaik.chunkymap.rendering.FileBufferRenderContext;
import de.lemaik.chunkymap.rendering.Renderer;
import de.lemaik.chunkymap.rendering.SilentTaskTracker;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
+import de.lemaik.chunkymap.rendering.rs.RemoteRenderer;
+import java.awt.image.DataBufferInt;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.World.Environment;
-import org.dynmap.Client;
-import org.dynmap.DynmapChunk;
-import org.dynmap.DynmapWorld;
-import org.dynmap.MapManager;
-import org.dynmap.MapTile;
+import org.dynmap.*;
+import org.dynmap.Client.Tile;
import org.dynmap.MapType.ImageVariant;
-import org.dynmap.MapTypeState;
import org.dynmap.hdmap.HDMapTile;
import org.dynmap.hdmap.HDPerspective;
import org.dynmap.hdmap.IsoHDPerspective;
@@ -25,29 +23,28 @@
import org.dynmap.storage.MapStorage;
import org.dynmap.storage.MapStorageTile;
import org.dynmap.utils.MapChunkCache;
+import se.llbit.chunky.entity.PlayerEntity;
+import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.World;
import se.llbit.chunky.world.World.LoggedWarnings;
+import se.llbit.util.ProgressListener;
+import se.llbit.util.TaskTracker;
public class ChunkyMapTile extends HDMapTile {
-
- private final ChunkyMap map;
-
- public ChunkyMapTile(DynmapWorld world, HDPerspective perspective, ChunkyMap map, int tx,
- int ty) {
- super(world, perspective, tx, ty, map.getBoostZoom());
- this.map = map;
+ public ChunkyMapTile(DynmapWorld world, HDPerspective perspective, int tx, int ty, int boostzoom, int tilescale) {
+ super(world, perspective, tx, ty, boostzoom, tilescale);
}
public ChunkyMapTile(DynmapWorld world, String parm) throws Exception {
// Do not remove this constructor! It is used by Dynmap to de-serialize tiles from the queue.
// The serialization happens in the inherited saveTileData() method.
super(world, parm);
- map = (ChunkyMap) world.maps.stream().filter(m -> m instanceof ChunkyMap).findFirst().get();
}
@Override
- public boolean render(MapChunkCache mapChunkCache, String s) {
+ public boolean render(MapChunkCache mapChunkCache, String mapName) {
+ final long startTimestamp = System.currentTimeMillis();
IsoHDPerspective perspective = (IsoHDPerspective) this.perspective;
final int scaled = (boostzoom > 0 && MarkerAPIImpl
@@ -55,6 +52,11 @@ public boolean render(MapChunkCache mapChunkCache, String s) {
128.0D)) ? boostzoom : 0;
// Mark the tiles we're going to render as validated
+ ChunkyMap map = (ChunkyMap) world.maps.stream()
+ .filter(m -> m instanceof ChunkyMap && (mapName == null || m.getName().equals(mapName))
+ && ((ChunkyMap) m).getPerspective() == perspective
+ && ((ChunkyMap) m).getBoostZoom() == boostzoom)
+ .findFirst().get();
MapTypeState mts = world.getMapState(map);
if (mts != null) {
mts.validateTile(tx, ty);
@@ -64,9 +66,9 @@ public boolean render(MapChunkCache mapChunkCache, String s) {
try {
Renderer renderer = map.getRenderer();
renderer.setDefaultTexturepack(map.getDefaultTexturepackPath());
- renderer.render(context, map.getTexturepackPath(), (scene) -> {
+ renderer.render(context, map.getResourcepackPaths(), (scene) -> {
org.bukkit.World bukkitWorld = Bukkit.getWorld(world.getRawName());
- World chunkyWorld = World.loadWorld(bukkitWorld.getWorldFolder(),
+ World chunkyWorld = World.loadWorld(map.getWorldFolder(world),
getChunkyDimension(bukkitWorld.getEnvironment()), LoggedWarnings.SILENT);
// Bukkit.getScheduler().runTask(ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class), Bukkit.getWorld(world.getRawName())::save);
map.applyTemplateScene(scene);
@@ -74,34 +76,107 @@ public boolean render(MapChunkCache mapChunkCache, String s) {
scene.setCanvasSize(128 * (1 << scaled), 128 * (1 << scaled));
scene.setTransparentSky(true);
scene.setYClipMin((int) perspective.minheight);
- if (perspective.maxheight > 0) {
- scene.setYClipMax((int) perspective.maxheight);
+ if (perspective.minheight == -2.147483648E9D) {
+ scene.setYClipMin(world.minY);
+ }
+ scene.setYClipMax((int) perspective.maxheight);
+ if (perspective.maxheight == -2.147483648E9D) {
+ if (world.isNether()) {
+ scene.setYClipMax(127);
+ } else {
+ scene.setYClipMax(world.worldheight - 1);
+ }
}
map.cameraAdapter.apply(scene.camera(), tx, ty, map.getMapZoomOutLevels(),
world.getExtraZoomOutLevels());
- scene.loadChunks(SilentTaskTracker.INSTANCE, chunkyWorld,
- perspective.getRequiredChunks(this).stream()
- .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream())
- .collect(Collectors.toList()));
+ if (renderer instanceof RemoteRenderer) {
+ if (((RemoteRenderer) renderer).shouldInitializeLocally()) {
+ Set chunks = perspective.getRequiredChunks(this).stream()
+ .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream())
+ .collect(Collectors.toSet());
+ Bukkit.getLogger().info("loading " + chunks.size()+ " chunks");
+ scene.setOctreeImplementation("PACKED");
+ scene.loadChunks(new TaskTracker((task, done, start, target, elapsedTime) -> {
+ Bukkit.getLogger().info(task + " (" + done + "/" + target + ")");
+ }), chunkyWorld, chunks);
+ Bukkit.getLogger().info("loaded " + chunks.size()+ " chunks");
+ scene.getActors().removeIf(actor -> actor instanceof PlayerEntity);
+ try {
+ scene.saveScene(context, new TaskTracker(ProgressListener.NONE));
+ } catch (IOException e) {
+ throw new RuntimeException("Could not save scene", e);
+ }
+ } else {
+ try {
+ Field chunks = Scene.class.getDeclaredField("chunks");
+ chunks.setAccessible(true);
+ Collection chunksList = (Collection) chunks.get(scene);
+ chunksList.clear();
+ chunksList.addAll(perspective.getRequiredChunks(this).stream()
+ .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream())
+ .collect(Collectors.toSet()));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException("Could not set chunks", e);
+ }
+ try {
+ Field worldPath = Scene.class.getDeclaredField("worldPath");
+ worldPath.setAccessible(true);
+ worldPath.set(scene, map.getWorldFolder(world).getAbsolutePath());
+ Field worldDimension = Scene.class.getDeclaredField("worldDimension");
+ worldDimension.setAccessible(true);
+ worldDimension.setInt(scene, bukkitWorld.getEnvironment().getId());
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException("Could not set world", e);
+ }
+ try {
+ scene.saveDescription(context.getSceneDescriptionOutputStream(scene.name));
+ } catch (IOException e) {
+ throw new RuntimeException("Could not save scene", e);
+ }
+ }
+ } else {
+ Set chunks = perspective.getRequiredChunks(this).stream()
+ .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream())
+ .collect(Collectors.toSet());
+ scene.loadChunks(SilentTaskTracker.INSTANCE, chunkyWorld, chunks);
+ }
}).thenApply((image) -> {
MapStorage var52 = world.getMapStorage();
MapStorageTile mtile = var52.getTile(world, map, tx, ty, 0, ImageVariant.STANDARD);
- try {
+ MapManager mapManager = MapManager.mapman;
+ boolean tileUpdated = false;
+ if (mapManager != null) {
+ DataBufferInt dataBuffer = (DataBufferInt) image.getRaster().getDataBuffer();
+ int[] data = dataBuffer.getData();
+ long crc = MapStorage.calculateImageHashCode(data, 0, data.length);
mtile.getWriteLock();
- mtile.write(image.hashCode(), image);
- MapManager.mapman.pushUpdate(getDynmapWorld(), new Client.Tile(mtile.getURI()));
- } finally {
- mtile.releaseWriteLock();
- MapManager.mapman.updateStatistics(this, map.getPrefix(), true, true, false);
+ try {
+ if (!mtile.matchesHashCode(crc)) {
+ mtile.write(crc, image, startTimestamp);
+ mapManager.pushUpdate(getDynmapWorld(), new Tile(mtile.getURI()));
+ tileUpdated = true;
+ }
+ } finally {
+ mtile.releaseWriteLock();
+ }
+ mapManager.updateStatistics(this, map.getPrefix(), true, true, false);
}
- return true;
+ return tileUpdated;
}).get();
return true;
} catch (Exception e) {
ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger()
- .log(Level.WARNING, "Rendering tile failed", e);
+ .log(Level.WARNING, "Rendering tile " + tx + "_" + ty + " failed", e);
+
+ if (map.getRequeueFailedTiles()) {
+ // Re-queue the failed tile
+ // Somewhat hacky but works surprisingly well
+ MapManager.mapman.tileQueue.push(this);
+ }
return false;
+ } finally {
+ context.dispose();
}
}
@@ -127,27 +202,19 @@ private static Collection getChunksAround(int centerX, int center
return chunks;
}
- @Override
- public List getRequiredChunks() {
- return map.getRequiredChunks(this);
- }
-
@Override
public MapTile[] getAdjecentTiles() {
- return map.getAdjecentTiles(this);
+ return ChunkyMap.getAdjecentTilesOfTile(this, perspective);
}
- public int hashCode() {
- return this.tx ^ this.ty ^ this.perspective.hashCode() ^ this.world.hashCode() ^ this.boostzoom;
- }
-
- public boolean equals(Object obj) {
- return obj instanceof ChunkyMapTile && this.equals((ChunkyMapTile) obj);
+ @Override
+ public boolean equals(HDMapTile o) {
+ return o instanceof ChunkyMapTile && o.tx == this.tx && o.ty == this.ty && this.perspective == o.perspective && ((ChunkyMapTile) o).world == this.world && o.boostzoom == this.boostzoom;
}
- public boolean equals(ChunkyMapTile o) {
- return o.tx == this.tx && o.ty == this.ty && this.perspective == o.perspective
- && o.world == this.world && o.boostzoom == this.boostzoom;
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof ChunkyMapTile&& ((ChunkyMapTile) o).tx == this.tx && ((ChunkyMapTile) o).ty == this.ty && this.perspective == ((ChunkyMapTile) o).perspective && ((ChunkyMapTile) o).world == this.world && ((ChunkyMapTile) o).boostzoom == this.boostzoom;
}
@Override
@@ -167,6 +234,11 @@ public boolean isRawBiomeDataNeeded() {
@Override
public boolean isBlockTypeDataNeeded() {
- return true;
+ return false;
+ }
+
+ @Override
+ protected String saveTileData() {
+ return String.format("%d,%d,%s,%d", this.tx, this.ty, this.perspective.getName(), this.boostzoom);
}
}
diff --git a/src/main/java/de/lemaik/chunkymap/dynmap/DynmapCameraAdapter.java b/src/main/java/de/lemaik/chunkymap/dynmap/DynmapCameraAdapter.java
index a989990..0a99fe5 100644
--- a/src/main/java/de/lemaik/chunkymap/dynmap/DynmapCameraAdapter.java
+++ b/src/main/java/de/lemaik/chunkymap/dynmap/DynmapCameraAdapter.java
@@ -8,36 +8,42 @@
import se.llbit.math.Vector3;
public class DynmapCameraAdapter {
+ private final double inclination;
+ private final double azimuth;
+ private final int scale;
+ private final Matrix3D transformMapToWorld;
- private final IsoHDPerspective perspective;
- private final Matrix3D transformMapToWorld;
+ public DynmapCameraAdapter(IsoHDPerspective perspective) {
+ this(perspective.inclination, perspective.azimuth, perspective.getModelScale());
+ }
- public DynmapCameraAdapter(IsoHDPerspective perspective) {
- this.perspective = perspective;
+ public DynmapCameraAdapter(double inclination, double azimuth, int scale) {
+ this.inclination = inclination;
+ this.azimuth = azimuth;
+ this.scale = scale;
+ transformMapToWorld = new Matrix3D();
+ transformMapToWorld.scale(1.0D / (double) scale,
+ 1.0D / (double) scale,
+ 1.0D / Math.sin(Math.toRadians(inclination)));
+ transformMapToWorld.shearZ(0.0D, -Math.tan(Math.toRadians(90.0D - inclination)));
+ transformMapToWorld.rotateYZ(-(90.0D - inclination));
+ transformMapToWorld.rotateXY(-180.0D + azimuth);
+ Matrix3D coordswap = new Matrix3D(0.0D, -1.0D, 0.0D, 0.0D, 0.0D, 1.0D, -1.0D, 0.0D, 0.0D);
+ transformMapToWorld.multiply(coordswap);
+ }
- transformMapToWorld = new Matrix3D();
- transformMapToWorld.scale(1.0D / (double) perspective.getModelScale(),
- 1.0D / (double) perspective.getModelScale(),
- 1.0D / Math.sin(Math.toRadians(perspective.inclination)));
- transformMapToWorld.shearZ(0.0D, -Math.tan(Math.toRadians(90.0D - perspective.inclination)));
- transformMapToWorld.rotateYZ(-(90.0D - perspective.inclination));
- transformMapToWorld.rotateXY(-180.0D + perspective.azimuth);
- Matrix3D coordswap = new Matrix3D(0.0D, -1.0D, 0.0D, 0.0D, 0.0D, 1.0D, -1.0D, 0.0D, 0.0D);
- transformMapToWorld.multiply(coordswap);
- }
+ public void apply(Camera camera, int tx, int ty, int mapzoomout, int extrazoomout) {
+ double x = tx + 0.5;
+ double y = ty + 0.5;
+ Vector3D v = new Vector3D(x * (1 << mapzoomout) * 64 / (double) scale,
+ y * (1 << mapzoomout) * 64 / (double) scale, 65);
+ transformMapToWorld.transform(v);
- public void apply(Camera camera, int tx, int ty, int mapzoomout, int extrazoomout) {
- double x = tx + 0.5;
- double y = ty + 0.5;
- Vector3D v = new Vector3D(x * (1 << mapzoomout) * 64 / perspective.getScale(),
- y * (1 << mapzoomout) * 64 / perspective.getScale(), 65);
- transformMapToWorld.transform(v);
-
- camera.setProjectionMode(ProjectionMode.PARALLEL);
- camera.setPosition(new Vector3(v.x, v.y, v.z));
- camera.setView((90 - perspective.azimuth + 90) / 180 * Math.PI,
- (-90 + perspective.inclination) / 180 * Math.PI, 0);
- camera.setFoV(128.0 / perspective.getScale());
- camera.setDof(Double.POSITIVE_INFINITY);
- }
+ camera.setProjectionMode(ProjectionMode.PARALLEL);
+ camera.setPosition(new Vector3(v.x, v.y, v.z));
+ camera.setView((90 - azimuth + 90) / 180 * Math.PI,
+ (-90 + inclination) / 180 * Math.PI, 0);
+ camera.setFoV(128.0 / (double) scale);
+ camera.setDof(Double.POSITIVE_INFINITY);
+ }
}
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java b/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java
index b2d5fe1..661f912 100644
--- a/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java
+++ b/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java
@@ -3,8 +3,9 @@
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
-import java.io.IOException;
import java.io.OutputStream;
+import java.lang.reflect.Field;
+
import se.llbit.chunky.main.Chunky;
import se.llbit.chunky.main.ChunkyOptions;
import se.llbit.chunky.renderer.RenderContext;
@@ -16,28 +17,29 @@
public class FileBufferRenderContext extends RenderContext {
private ByteArrayOutputStream scene;
- private ByteArrayOutputStream grass;
- private ByteArrayOutputStream foliage;
private ByteArrayOutputStream octree;
public FileBufferRenderContext() {
super(new Chunky(ChunkyOptions.getDefaults()));
+ try {
+ Field headless = Chunky.class.getDeclaredField("headless");
+ headless.setAccessible(true);
+ headless.set(this.getChunky(), true);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
public OutputStream getSceneFileOutputStream(String fileName) throws FileNotFoundException {
if (fileName.endsWith(".json")) {
return scene = new ByteArrayOutputStream();
- } else if (fileName.endsWith(".grass")) {
- return grass = new ByteArrayOutputStream();
- } else if (fileName.endsWith(".foliage")) {
- return foliage = new ByteArrayOutputStream();
- } else if (fileName.endsWith(".octree")) {
+ } else if (fileName.endsWith(".octree2")) {
return octree = new ByteArrayOutputStream();
} else {
return new OutputStream() {
@Override
- public void write(int b) throws IOException {
+ public void write(int b) {
// no-op
}
};
@@ -48,14 +50,6 @@ public byte[] getScene() {
return scene.toByteArray();
}
- public byte[] getGrass() {
- return grass.toByteArray();
- }
-
- public byte[] getFoliage() {
- return foliage.toByteArray();
- }
-
public byte[] getOctree() {
return octree.toByteArray();
}
@@ -63,4 +57,9 @@ public byte[] getOctree() {
public void setRenderThreadCount(int threads) {
config.renderThreads = threads;
}
+
+ public void dispose() {
+ scene = null;
+ octree = null;
+ }
}
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java b/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java
index 3d101ce..b970e61 100644
--- a/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java
+++ b/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java
@@ -8,4 +8,8 @@ public class RenderException extends Exception {
public RenderException(String message, Throwable inner) {
super(message, inner);
}
+
+ public RenderException(String message) {
+ super(message);
+ }
}
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java b/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java
index e6c4639..133d1c4 100644
--- a/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java
+++ b/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java
@@ -2,6 +2,7 @@
import java.awt.image.BufferedImage;
import java.io.File;
+import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import se.llbit.chunky.renderer.scene.Scene;
@@ -15,12 +16,13 @@ public interface Renderer {
* Renders a scene using the given context.
*
* @param context render context
- * @param texturepack the texturepack
+ * @param texturepacks the texturepacks
* @param initializeScene function that initializes the scene
* @return future with the rendered image
*/
- CompletableFuture render(FileBufferRenderContext context, File texturepack,
- Consumer initializeScene);
+ CompletableFuture render(FileBufferRenderContext context, File[] texturepacks,
+ Consumer initializeScene)
+ throws IOException;
/**
* set the default / fallback texturepack to use
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/SilentTaskTracker.java b/src/main/java/de/lemaik/chunkymap/rendering/SilentTaskTracker.java
index 9c0d4a3..ff5c7a9 100644
--- a/src/main/java/de/lemaik/chunkymap/rendering/SilentTaskTracker.java
+++ b/src/main/java/de/lemaik/chunkymap/rendering/SilentTaskTracker.java
@@ -3,6 +3,8 @@
import se.llbit.util.ProgressListener;
import se.llbit.util.TaskTracker;
+import java.time.Duration;
+
public class SilentTaskTracker extends TaskTracker {
public static final TaskTracker INSTANCE = new SilentTaskTracker();
@@ -10,12 +12,12 @@ public class SilentTaskTracker extends TaskTracker {
private SilentTaskTracker() {
super(new ProgressListener() {
@Override
- public void setProgress(String s, int i, int i1, int i2) {
+ public void setProgress(String task, int done, int start, int target, Duration elapsedTime) {
// empty
}
@Override
- public void setProgress(String s, int i, int i1, int i2, String s1) {
+ public void setProgress(String task, int done, int start, int target, Duration elapsedTime, Duration remainingTime) {
// empty
}
});
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyLogAdapter.java b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyLogAdapter.java
new file mode 100644
index 0000000..0c32e35
--- /dev/null
+++ b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyLogAdapter.java
@@ -0,0 +1,89 @@
+package de.lemaik.chunkymap.rendering.local;
+
+import java.util.logging.Logger;
+import se.llbit.log.Level;
+import se.llbit.log.Receiver;
+
+/**
+ * Adapter for Chunky's logger that redirects log messages to the plugin's logger and suppresses
+ * known false-positive warnings.
+ */
+public class ChunkyLogAdapter extends Receiver {
+
+ private Logger logger;
+
+ public ChunkyLogAdapter(Logger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public void logEvent(Level level, String message) {
+ if (this.shouldIgnoreMessage(level, message, null)) {
+ return;
+ }
+ switch (level) {
+ case ERROR:
+ logger.severe(message);
+ break;
+ case WARNING:
+ logger.warning(message);
+ break;
+ case INFO:
+ default:
+ logger.info(message);
+ break;
+ }
+ }
+
+ @Override
+ public void logEvent(Level level, String message, Throwable thrown) {
+ if (this.shouldIgnoreMessage(level, message, thrown)) {
+ return;
+ }
+ switch (level) {
+ case ERROR:
+ logger.log(java.util.logging.Level.SEVERE, message, thrown);
+ break;
+ case WARNING:
+ logger.log(java.util.logging.Level.WARNING, message, thrown);
+ break;
+ case INFO:
+ default:
+ logger.log(java.util.logging.Level.INFO, message, thrown);
+ break;
+ }
+ }
+
+ @Override
+ public void logEvent(Level level, Throwable thrown) {
+ if (this.shouldIgnoreMessage(level, null, thrown)) {
+ return;
+ }
+ switch (level) {
+ case ERROR:
+ logger.log(java.util.logging.Level.SEVERE, thrown.getMessage(), thrown);
+ break;
+ case WARNING:
+ logger.log(java.util.logging.Level.WARNING, thrown.getMessage(), thrown);
+ break;
+ case INFO:
+ default:
+ logger.log(java.util.logging.Level.INFO, thrown.getMessage(), thrown);
+ }
+ }
+
+ protected boolean shouldIgnoreMessage(Level level, String message, Throwable thrown) {
+ if (message == null) {
+ return false;
+ }
+ if (message.startsWith("Warning: Could not load settings from")) {
+ // this is intended
+ return true;
+ }
+ if (message.startsWith("Unknown biome")) {
+ // don't spam the user about this
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java
index 363d1a3..862163b 100644
--- a/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java
+++ b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java
@@ -1,267 +1,155 @@
package de.lemaik.chunkymap.rendering.local;
-import de.lemaik.chunky.denoiser.AlbedoTracer;
-import de.lemaik.chunky.denoiser.CombinedRayTracer;
-import de.lemaik.chunky.denoiser.NormalTracer;
+import de.lemaik.chunky.denoiser.DenoisedPathTracingRenderer;
+import de.lemaik.chunky.denoiser.DenoiserSettings;
import de.lemaik.chunkymap.ChunkyMapPlugin;
import de.lemaik.chunkymap.rendering.FileBufferRenderContext;
import de.lemaik.chunkymap.rendering.RenderException;
import de.lemaik.chunkymap.rendering.Renderer;
import de.lemaik.chunkymap.rendering.SilentTaskTracker;
-import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.io.File;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.nio.FloatBuffer;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import net.time4tea.oidn.Oidn;
-import net.time4tea.oidn.Oidn.DeviceType;
-import net.time4tea.oidn.OidnDevice;
-import net.time4tea.oidn.OidnFilter;
-import net.time4tea.oidn.OidnImages;
import se.llbit.chunky.PersistentSettings;
-import se.llbit.chunky.renderer.Postprocess;
+import se.llbit.chunky.main.Chunky;
import se.llbit.chunky.renderer.RenderManager;
import se.llbit.chunky.renderer.SnapshotControl;
+import se.llbit.chunky.renderer.scene.AlphaBuffer;
import se.llbit.chunky.renderer.scene.PathTracer;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.renderer.scene.SynchronousSceneManager;
-import se.llbit.chunky.resources.BitmapImage;
-import se.llbit.chunky.resources.TexturePackLoader;
+import se.llbit.chunky.resources.ResourcePackLoader;
import se.llbit.util.TaskTracker;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
/**
* A renderer that uses Chunky to render scenes locally.
*/
public class ChunkyRenderer implements Renderer {
- private static File previousTexturepack;
- private static File previousDefaultTexturepack;
- private final int targetSpp;
- private final boolean enableDenoiser;
- private final int albedoTargetSpp;
- private final int normalTargetSpp;
- private final int threads;
- private final int cpuLoad;
-
- public ChunkyRenderer(int targetSpp, boolean enableDenoiser, int albedoTargetSpp,
- int normalTargetSpp, int threads, int cpuLoad) {
- this.targetSpp = targetSpp;
- this.enableDenoiser = enableDenoiser;
- this.albedoTargetSpp = albedoTargetSpp;
- this.normalTargetSpp = normalTargetSpp;
- this.threads = threads;
- this.cpuLoad = cpuLoad;
-
- PersistentSettings.changeSettingsDirectory(
- new File(ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getDataFolder(), "chunky"));
- PersistentSettings.setLoadPlayers(false);
- }
-
- @Override
- public void setDefaultTexturepack(File texturepack) {
- // no-op, textures are static in Chunky and were already loaded
- }
-
- public static void loadTexturepack(File texturepack) {
- if (!texturepack.equals(previousTexturepack)) {
- // this means that only one texturepack can be used for all maps, if rendering with multiple chunky instances
- TexturePackLoader.loadTexturePacks(texturepack.getAbsolutePath(), false);
- previousTexturepack = texturepack;
+ private static List previousTexturepacks;
+ private File defaultTexturepack;
+ private final int targetSpp;
+ private final boolean enableDenoiser;
+ private final int albedoTargetSpp;
+ private final int normalTargetSpp;
+ private final int threads;
+ private final int cpuLoad;
+
+ static {
+ Chunky.addRenderer(new DenoisedPathTracingRenderer(
+ new Oidn4jDenoiser(), "Oidn4jDenoisedPathTracer", "DenoisedPathTracer", "DenoisedPathTracer", new PathTracer()));
}
- }
- public static void loadDefaultTexturepack(File texturepack) {
- if (!texturepack.equals(previousDefaultTexturepack)) {
- // this means that only one texturepack can be used for all maps, if rendering with multiple chunky instances
- TexturePackLoader.loadTexturePacks(texturepack.getAbsolutePath(), false);
- previousDefaultTexturepack = texturepack;
+ public ChunkyRenderer(int targetSpp, boolean enableDenoiser, int albedoTargetSpp,
+ int normalTargetSpp, int threads, int cpuLoad) {
+ this.targetSpp = targetSpp;
+ this.enableDenoiser = enableDenoiser;
+ this.albedoTargetSpp = albedoTargetSpp;
+ this.normalTargetSpp = normalTargetSpp;
+ this.threads = threads;
+ this.cpuLoad = cpuLoad;
+
+ PersistentSettings.changeSettingsDirectory(
+ new File(ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getDataFolder(), "chunky"));
+ PersistentSettings.setLoadPlayers(false);
+ PersistentSettings.setDisableDefaultTextures(true);
}
- }
-
- @Override
- public CompletableFuture render(FileBufferRenderContext context, File texturepack,
- Consumer initializeScene) {
- CompletableFuture result = new CompletableFuture<>();
- CombinedRayTracer combinedRayTracer = new CombinedRayTracer();
- context.getChunky().setRayTracerFactory(() -> combinedRayTracer);
- context.setRenderThreadCount(threads);
- RenderManager renderer = new RenderManager(context, false);
- renderer.setCPULoad(cpuLoad);
-
- SynchronousSceneManager sceneManager = new SynchronousSceneManager(context, renderer);
- initializeScene.accept(sceneManager.getScene());
- renderer.setSceneProvider(sceneManager);
- renderer.setSnapshotControl(new SnapshotControl() {
- @Override
- public boolean saveSnapshot(Scene scene, int nextSpp) {
- return false;
- }
+ @Override
+ public void setDefaultTexturepack(File texturepack) {
+ defaultTexturepack = texturepack;
+ }
- @Override
- public boolean saveRenderDump(Scene scene, int nextSpp) {
- return false;
- }
- });
+ @Override
+ public CompletableFuture render(FileBufferRenderContext context, File[] texturepacks,
+ Consumer initializeScene) {
+ CompletableFuture result = new CompletableFuture<>();
- AtomicReference albedo = new AtomicReference<>();
- AtomicReference normal = new AtomicReference<>();
+ List resourcepacks = new ArrayList<>(texturepacks.length);
+ if (defaultTexturepack != null) {
+ resourcepacks.add(defaultTexturepack);
+ }
+ if (!resourcepacks.equals(previousTexturepacks)) {
+ ResourcePackLoader.loadResourcePacks(resourcepacks);
+ previousTexturepacks = resourcepacks;
+ }
- renderer.setOnRenderCompleted((time, sps) -> {
- try {
- if (combinedRayTracer.getRayTracer() instanceof PathTracer) {
- result.complete(getImage(sceneManager.getScene(), albedo.get(), normal.get()));
- renderer.interrupt();
- } else if (combinedRayTracer.getRayTracer() instanceof AlbedoTracer) {
- Scene scene = renderer.getBufferedScene();
- double[] samples = scene.getSampleBuffer();
- FloatBuffer albedoBuffer = Oidn.Companion.allocateBuffer(sceneManager.getScene().width,
- sceneManager.getScene().height);
- for (int y = 0; y < scene.height; y++) {
- for (int x = 0; x < scene.width; x++) {
- albedoBuffer.put((y * scene.width + x) * 3,
- (float) Math.min(1.0, samples[(y * scene.width + x) * 3 + 0]));
- albedoBuffer.put((y * scene.width + x) * 3 + 1,
- (float) Math.min(1.0, samples[(y * scene.width + x) * 3 + 1]));
- albedoBuffer.put((y * scene.width + x) * 3 + 2,
- (float) Math.min(1.0, samples[(y * scene.width + x) * 3 + 2]));
+ context.setRenderThreadCount(threads);
+ RenderManager renderManager = context.getChunky().getRenderController().getRenderManager();
+ renderManager.setCPULoad(cpuLoad);
+
+ SynchronousSceneManager sceneManager = new SynchronousSceneManager(context, renderManager);
+ sceneManager.withEditSceneProtected(initializeScene);
+ sceneManager.applySceneChanges();
+
+ DenoiserSettings settings = new DenoiserSettings();
+ settings.renderAlbedo.set(albedoTargetSpp > 0);
+ settings.albedoSpp.set(albedoTargetSpp);
+ settings.renderNormal.set(normalTargetSpp > 0);
+ settings.normalSpp.set(normalTargetSpp);
+ settings.saveToScene(sceneManager.getScene());
+
+ renderManager.setSceneProvider(sceneManager);
+ renderManager.setSnapshotControl(new SnapshotControl() {
+ @Override
+ public boolean saveSnapshot(Scene scene, int nextSpp) {
+ return false;
}
- }
- albedo.set(albedoBuffer);
- combinedRayTracer.setRayTracer(new PathTracer());
- sceneManager.getScene().haltRender();
- sceneManager.getScene().setTargetSpp(targetSpp);
- sceneManager.getScene().startHeadlessRender();
- } else if (combinedRayTracer.getRayTracer() instanceof NormalTracer) {
- Scene scene = renderer.getBufferedScene();
- double[] samples = scene.getSampleBuffer();
- FloatBuffer normalBuffer = Oidn.Companion.allocateBuffer(sceneManager.getScene().width,
- sceneManager.getScene().height);
- for (int y = 0; y < scene.height; y++) {
- for (int x = 0; x < scene.width; x++) {
- normalBuffer.put((y * scene.width + x) * 3,
- (float) samples[(y * scene.width + x) * 3 + 0]);
- normalBuffer.put((y * scene.width + x) * 3 + 1,
- (float) samples[(y * scene.width + x) * 3 + 1]);
- normalBuffer.put((y * scene.width + x) * 3 + 2,
- (float) samples[(y * scene.width + x) * 3 + 2]);
+ @Override
+ public boolean saveRenderDump(Scene scene, int nextSpp) {
+ return false;
}
- }
- normal.set(normalBuffer);
+ });
- combinedRayTracer.setRayTracer(new AlbedoTracer());
- sceneManager.getScene().haltRender();
- sceneManager.getScene().setTargetSpp(albedoTargetSpp);
- sceneManager.getScene().startHeadlessRender();
+ try {
+ if (enableDenoiser) {
+ sceneManager.getScene().setRenderer("Oidn4jDenoisedPathTracer");
+ }
+ sceneManager.getScene().haltRender();
+ sceneManager.getScene().setTargetSpp(targetSpp);
+ renderManager.start();
+ sceneManager.getScene().startRender();
+ renderManager.join();
+ result.complete(getImage(sceneManager.getScene()));
+ } catch (InterruptedException | ReflectiveOperationException e) {
+ result.completeExceptionally(new RenderException("Rendering failed", e));
+ } finally {
+ renderManager.shutdown();
}
- } catch (ReflectiveOperationException e) {
- result
- .completeExceptionally(new RenderException("Could not get final image from Chunky", e));
- }
- });
-
- try {
- if (enableDenoiser && normalTargetSpp > 0) {
- combinedRayTracer.setRayTracer(new NormalTracer());
- sceneManager.getScene().setTargetSpp(normalTargetSpp);
- } else if (enableDenoiser && albedoTargetSpp > 0) {
- combinedRayTracer.setRayTracer(new AlbedoTracer());
- sceneManager.getScene().setTargetSpp(albedoTargetSpp);
- } else {
- sceneManager.getScene().setTargetSpp(targetSpp);
- }
- sceneManager.getScene().startHeadlessRender();
- renderer.start();
- renderer.join();
- renderer.shutdown();
- } catch (InterruptedException e) {
- result.completeExceptionally(new RenderException("Rendering failed", e));
- } finally {
- renderer.shutdown();
+ return result;
}
- return result;
- }
-
- private BufferedImage getImage(Scene scene, FloatBuffer albedo, FloatBuffer normal)
- throws ReflectiveOperationException {
- if (enableDenoiser) {
- double[] samples = scene.getSampleBuffer();
- FloatBuffer buffer = Oidn.Companion.allocateBuffer(scene.width, scene.height);
-
- // TODO use multiple threads for post-processing
- for (int y = 0; y < scene.height; y++) {
- for (int x = 0; x < scene.width; x++) {
- double[] result = new double[3];
- if (scene.getPostprocess() != Postprocess.NONE) {
- scene.postProcessPixel(x, y, result);
- } else {
- result[0] = samples[(y * scene.width + x) * 3 + 0];
- result[1] = samples[(y * scene.width + x) * 3 + 1];
- result[2] = samples[(y * scene.width + x) * 3 + 2];
- }
- buffer.put((y * scene.width + x) * 3, (float) Math.min(1.0, result[0]));
- buffer.put((y * scene.width + x) * 3 + 1, (float) Math.min(1.0, result[1]));
- buffer.put((y * scene.width + x) * 3 + 2, (float) Math.min(1.0, result[2]));
- }
- }
-
- Oidn oidn = new Oidn();
- try (OidnDevice device = oidn.newDevice(DeviceType.DEVICE_TYPE_DEFAULT)) {
- try (OidnFilter filter = device.raytraceFilter()) {
- filter.setFilterImage(buffer, buffer, scene.width, scene.height);
- if (albedo != null) {
- // albedo is required if normal is set
- filter.setAdditionalImages(albedo, normal, scene.width, scene.height);
- }
- filter.commit();
- filter.execute();
+ private BufferedImage getImage(Scene scene)
+ throws ReflectiveOperationException {
+ Class sceneClass = Scene.class;
+ Method computeAlpha = AlphaBuffer.class
+ .getDeclaredMethod("computeAlpha", new Class[]{Scene.class, AlphaBuffer.Type.class, TaskTracker.class});
+ computeAlpha.setAccessible(true);
+ computeAlpha.invoke(scene.getAlphaBuffer(), new Object[]{scene, AlphaBuffer.Type.UINT8, TaskTracker.NONE});
+
+ Field finalized = sceneClass.getDeclaredField("finalized");
+ finalized.setAccessible(true);
+ if (!finalized.getBoolean(scene)) {
+ scene.postProcessFrame(SilentTaskTracker.INSTANCE);
}
- }
-
- BufferedImage renderedImage = OidnImages.Companion
- .newBufferedImage(scene.width, scene.height);
- for (int i = 0; i < buffer.capacity(); i++) {
- renderedImage.getRaster().getDataBuffer().setElemFloat(i, buffer.get(i));
- }
- BufferedImage imageInIntPixelLayout = new BufferedImage(scene.width, scene.height,
- BufferedImage.TYPE_INT_ARGB);
- Graphics2D graphics = imageInIntPixelLayout.createGraphics();
- graphics.drawImage(renderedImage, 0, 0, null);
- graphics.dispose();
-
- return imageInIntPixelLayout;
- } else {
- Class sceneClass = Scene.class;
- Method computeAlpha = sceneClass
- .getDeclaredMethod("computeAlpha", new Class[]{TaskTracker.class, int.class});
- computeAlpha.setAccessible(true);
- computeAlpha.invoke(scene, SilentTaskTracker.INSTANCE, threads);
-
- Field finalized = sceneClass.getDeclaredField("finalized");
- finalized.setAccessible(true);
- if (!finalized.getBoolean(scene)) {
- scene.postProcessFrame(SilentTaskTracker.INSTANCE, threads);
- }
-
- Field backBuffer = sceneClass.getDeclaredField("backBuffer");
- backBuffer.setAccessible(true);
- BitmapImage bitmap = (BitmapImage) backBuffer.get(scene);
- BufferedImage renderedImage = new BufferedImage(bitmap.width, bitmap.height,
- BufferedImage.TYPE_INT_ARGB);
- DataBufferInt dataBuffer = (DataBufferInt) renderedImage.getRaster().getDataBuffer();
- int[] data = dataBuffer.getData();
- System.arraycopy(bitmap.data, 0, data, 0, bitmap.width * bitmap.height);
+ scene.swapBuffers();
+ BufferedImage renderedImage = new BufferedImage(scene.canvasConfig.getWidth(), scene.canvasConfig.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ scene.withBufferedImage(bitmap -> {
+ DataBufferInt dataBuffer = (DataBufferInt) renderedImage.getRaster().getDataBuffer();
+ int[] data = dataBuffer.getData();
+ System.arraycopy(bitmap.data, 0, data, 0, bitmap.width * bitmap.height);
+ });
- return renderedImage;
+ return renderedImage;
}
- }
}
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/local/Oidn4jDenoiser.java b/src/main/java/de/lemaik/chunkymap/rendering/local/Oidn4jDenoiser.java
new file mode 100644
index 0000000..2e30efb
--- /dev/null
+++ b/src/main/java/de/lemaik/chunkymap/rendering/local/Oidn4jDenoiser.java
@@ -0,0 +1,70 @@
+package de.lemaik.chunkymap.rendering.local;
+
+import de.lemaik.chunky.denoiser.Denoiser;
+import de.lemaik.chunkymap.ChunkyMapPlugin;
+import net.time4tea.oidn.Oidn;
+import net.time4tea.oidn.OidnDevice;
+import net.time4tea.oidn.OidnFilter;
+
+import java.nio.FloatBuffer;
+import java.util.logging.Level;
+
+public class Oidn4jDenoiser implements Denoiser {
+ @Override
+ public float[] denoise(int width, int height, float[] beauty, float[] albedo, float[] normal) {
+ FloatBuffer albedoBuffer = Oidn.Companion.allocateBuffer(width, height);
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ albedoBuffer.put((y * width + x) * 3, albedo[(y * width + x) * 3 + 0]);
+ albedoBuffer.put((y * width + x) * 3 + 1, albedo[(y * width + x) * 3 + 1]);
+ albedoBuffer.put((y * width + x) * 3 + 2, albedo[(y * width + x) * 3 + 2]);
+ }
+ }
+
+ FloatBuffer normalBuffer = Oidn.Companion.allocateBuffer(width, height);
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ normalBuffer.put((y * width + x) * 3, normal[(y * width + x) * 3 + 0]);
+ normalBuffer.put((y * width + x) * 3 + 1, normal[(y * width + x) * 3 + 1]);
+ normalBuffer.put((y * width + x) * 3 + 2, normal[(y * width + x) * 3 + 2]);
+ }
+ }
+
+ FloatBuffer buffer = Oidn.Companion.allocateBuffer(width, height);
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ buffer.put((y * width + x) * 3, beauty[(y * width + x) * 3 + 0]);
+ buffer.put((y * width + x) * 3 + 1, beauty[(y * width + x) * 3 + 1]);
+ buffer.put((y * width + x) * 3 + 2, beauty[(y * width + x) * 3 + 2]);
+ }
+ }
+
+ Oidn oidn = new Oidn();
+ try (OidnDevice device = oidn.newDevice(Oidn.DeviceType.DEVICE_TYPE_DEFAULT)) {
+ try (OidnFilter filter = device.raytraceFilter()) {
+ filter.setFilterImage(buffer, buffer, width, height);
+ if (albedo != null) {
+ // albedo is required if normal is set
+ filter.setAdditionalImages(albedoBuffer, normalBuffer, width, height);
+ }
+ filter.commit();
+ filter.execute();
+
+ if (!device.error().ok()) {
+ ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger().log(Level.WARNING, "Denoiser failed: " + device.error().getMessage() + " " + device.error().getError().getExplanation());
+ }
+ }
+ } catch (Exception e) {
+ ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger().log(Level.WARNING, "Denoiser failed", e);
+ }
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ beauty[(y * width + x) * 3] = buffer.get((y * width + x) * 3);
+ beauty[(y * width + x) * 3 + 1] = buffer.get((y * width + x) * 3 + 1);
+ beauty[(y * width + x) * 3 + 2] = buffer.get((y * width + x) * 3 + 2);
+ }
+ }
+ return beauty;
+ }
+}
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java b/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java
index 50b95b9..47d6d24 100644
--- a/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java
+++ b/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java
@@ -20,13 +20,23 @@
package de.lemaik.chunkymap.rendering.rs;
import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import de.lemaik.chunkymap.rendering.RenderException;
+import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
-import java.io.InputStreamReader;
+import java.io.Reader;
import java.net.URL;
+import java.util.List;
+import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import okhttp3.Call;
import okhttp3.Callback;
@@ -36,7 +46,7 @@
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
-import okio.Buffer;
+import okhttp3.ResponseBody;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
@@ -48,32 +58,42 @@ public class ApiClient {
private final String baseUrl;
private final OkHttpClient client;
- public ApiClient(String baseUrl) {
+ public ApiClient(String baseUrl, String apiKey) {
this.baseUrl = baseUrl;
- client = new OkHttpClient.Builder().build();
+ client = new OkHttpClient.Builder()
+ .addInterceptor(chain -> chain.proceed(
+ chain.request().newBuilder()
+ .header("X-Api-Key", apiKey)
+ .header("User-Agent", "ChunkyMap")
+ .build()))
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .writeTimeout(15, TimeUnit.MINUTES)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .build();
}
- public CompletableFuture createJob(byte[] scene, byte[] octree, byte[] grass,
- byte[] foliage, byte[] skymap, String skymapName, TaskTracker taskTracker) {
+ public CompletableFuture createJob(byte[] scene, byte[] octree, byte[] skymap,
+ String skymapName, String texturepack, int targetSpp, TaskTracker taskTracker) {
CompletableFuture result = new CompletableFuture<>();
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()
.setType(MediaType.parse("multipart/form-data"))
- .addFormDataPart("foliage", "scene.foliage",
- byteBody(foliage, () -> taskTracker.task("Upload foliage...")))
- .addFormDataPart("grass", "scene.grass",
- byteBody(grass, () -> taskTracker.task("Upload task...")))
.addFormDataPart("scene", "scene.json",
byteBody(scene, () -> taskTracker.task("Upload scene...")))
- .addFormDataPart("octree", "scene.octree",
+ .addFormDataPart("octree", "scene.octree2",
byteBody(octree, () -> taskTracker.task("Upload octree...")))
- .addFormDataPart("targetSpp", "100");
+ .addFormDataPart("targetSpp", "" + targetSpp)
+ .addFormDataPart("transient", "true");
if (skymap != null) {
multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymapName,
byteBody(skymap, () -> taskTracker.task("Upload skymap...")));
}
+ if (texturepack != null) {
+ multipartBuilder = multipartBuilder.addFormDataPart("texturepack", texturepack);
+ }
+
client.newCall(new Request.Builder()
.url(baseUrl + "/jobs")
.post(multipartBuilder.build())
@@ -85,15 +105,23 @@ public void onFailure(Call call, IOException e) {
}
@Override
- public void onResponse(Call call, Response response) throws IOException {
- if (response.code() == 201) {
- try (InputStreamReader reader = new InputStreamReader(response.body().byteStream())) {
- result.complete(gson.fromJson(reader, RenderJob.class));
- } catch (IOException e) {
- result.completeExceptionally(e);
+ public void onResponse(Call call, Response response) {
+ try {
+ if (response.code() == 201) {
+ try (
+ ResponseBody body = response.body();
+ Reader reader = body.charStream()
+ ) {
+ result.complete(gson.fromJson(reader, RenderJob.class));
+ } catch (IOException e) {
+ result.completeExceptionally(e);
+ }
+ } else {
+ result
+ .completeExceptionally(new IOException("The render job could not be created"));
}
- } else {
- result.completeExceptionally(new IOException("The render job could not be created"));
+ } finally {
+ response.close();
}
}
});
@@ -101,25 +129,104 @@ public void onResponse(Call call, Response response) throws IOException {
return result;
}
- public CompletableFuture createJob(File scene, File octree, File grass, File foliage,
- File skymap, TaskTracker taskTracker) throws IOException {
+ private CompletableFuture createJob(byte[] scene, List regionFiles,
+ JsonObject cachedRegions, byte[] skymap,
+ String skymapName, String texturepack, int targetSpp, TaskTracker taskTracker) {
CompletableFuture result = new CompletableFuture<>();
+ JsonObject regions = new JsonObject();
+ for (Entry entry : cachedRegions.entrySet()) {
+ if (regionFiles.stream().noneMatch(file -> file.getName().equals(entry.getKey()))) {
+ // not submitted as file
+ regions.add(entry.getKey(), entry.getValue());
+ }
+ }
+
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()
.setType(MediaType.parse("multipart/form-data"))
- .addFormDataPart("foliage", "scene.foliage",
- fileBody(foliage, () -> taskTracker.task("Upload foliage...")))
- .addFormDataPart("grass", "scene.grass",
- fileBody(grass, () -> taskTracker.task("Upload task...")))
.addFormDataPart("scene", "scene.json",
- fileBody(scene, () -> taskTracker.task("Upload scene...")))
- .addFormDataPart("octree", "scene.octree",
- fileBody(octree, () -> taskTracker.task("Upload octree...")))
- .addFormDataPart("targetSpp", "100");
+ byteBody(scene, () -> taskTracker.task("Upload scene...")))
+ .addFormDataPart("targetSpp", "" + targetSpp)
+ .addFormDataPart("transient", "true")
+ .addFormDataPart("cachedRegions", regions.toString());
+
+ for (File region : regionFiles) {
+ multipartBuilder = multipartBuilder.addFormDataPart("region", region.getName(),
+ fileBody(region, () -> taskTracker.task("Upload region " + region.getName())));
+ }
if (skymap != null) {
- multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymap.getName(),
- fileBody(skymap, () -> taskTracker.task("Upload skymap...")));
+ multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymapName,
+ byteBody(skymap, () -> taskTracker.task("Upload skymap...")));
+ }
+
+ if (texturepack != null) {
+ multipartBuilder = multipartBuilder.addFormDataPart("texturepack", texturepack);
+ }
+
+ client.newCall(new Request.Builder()
+ .url(baseUrl + "/jobs")
+ .post(multipartBuilder.build())
+ .build())
+ .enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ result.completeExceptionally(e);
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ try {
+ if (response.code() == 201) {
+ try (
+ ResponseBody body = response.body();
+ Reader reader = body.charStream()
+ ) {
+ result.complete(gson.fromJson(reader, RenderJob.class));
+ } catch (IOException e) {
+ result.completeExceptionally(e);
+ }
+ } else {
+ result.completeExceptionally(
+ new IOException(
+ "The render job could not be created: " + response.code() + " " + response
+ .message()));
+ }
+ } finally {
+ response.close();
+ }
+ }
+ });
+
+ return result;
+ }
+
+ public CompletableFuture createJob(byte[] scene, List regionFiles, byte[] skymap,
+ String skymapName, String texturepack, int targetSpp, TaskTracker taskTracker)
+ throws IOException {
+ CompletableFuture result = new CompletableFuture<>();
+
+ JsonObject regions = new JsonObject();
+ for (File region : regionFiles) {
+ regions.addProperty(region.getName(),
+ Okio.buffer(Okio.source(region)).readByteString().md5().hex());
+ }
+
+ MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()
+ .setType(MediaType.parse("multipart/form-data"))
+ .addFormDataPart("scene", "scene.json",
+ byteBody(scene, () -> taskTracker.task("Upload scene...")))
+ .addFormDataPart("targetSpp", "" + targetSpp)
+ .addFormDataPart("transient", "true")
+ .addFormDataPart("cachedRegions", regions.toString());
+
+ if (skymap != null) {
+ multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymapName,
+ byteBody(skymap, () -> taskTracker.task("Upload skymap...")));
+ }
+
+ if (texturepack != null) {
+ multipartBuilder = multipartBuilder.addFormDataPart("texturepack", texturepack);
}
client.newCall(new Request.Builder()
@@ -134,14 +241,63 @@ public void onFailure(Call call, IOException e) {
@Override
public void onResponse(Call call, Response response) throws IOException {
- if (response.code() == 201) {
- try (InputStreamReader reader = new InputStreamReader(response.body().byteStream())) {
- result.complete(gson.fromJson(reader, RenderJob.class));
- } catch (IOException e) {
- result.completeExceptionally(e);
+ try {
+ if (response.code() == 201) {
+ try (
+ ResponseBody body = response.body();
+ Reader reader = body.charStream()
+ ) {
+ result.complete(gson.fromJson(reader, RenderJob.class));
+ } catch (IOException e) {
+ result.completeExceptionally(e);
+ }
+ } else if (response.code() == 400) {
+ try (
+ ResponseBody body = response.body();
+ Reader reader = body.charStream()
+ ) {
+ JsonObject obj = gson.fromJson(reader, JsonObject.class);
+ if (obj.has("missing")) {
+ try {
+ ApiClient.this.createJob(scene, regionFiles.stream().filter(
+ file -> obj.getAsJsonArray("missing")
+ .contains(new JsonPrimitive(file.getName()))).collect(
+ Collectors.toList()), regions, skymap, skymapName, texturepack, targetSpp,
+ taskTracker).whenComplete((job, ex) -> {
+ if (ex == null) {
+ result.complete(job);
+ } else {
+ result.completeExceptionally(ex);
+ }
+ });
+ } catch (Exception e) {
+ result.completeExceptionally(e);
+ }
+ } else {
+ result.completeExceptionally(
+ new IOException(
+ "The render job could not be created: " + response.code() + " "
+ + response.message() + " " + obj.toString()));
+ }
+ } catch (JsonParseException e) {
+ result.completeExceptionally(e);
+ }
+ } else {
+ String responseBody = "";
+ ResponseBody body = response.body();
+ if (body != null) {
+ try {
+ responseBody = body.string();
+ } catch (IOException e) {
+ }
+ }
+ result.completeExceptionally(
+ new IOException(
+ "The render job could not be created: " + response.code() + " "
+ + response.message() + " " + responseBody));
}
- } else {
- result.completeExceptionally(new IOException("The render job could not be created"));
+ } finally {
+ response.close();
}
}
});
@@ -149,18 +305,26 @@ public void onResponse(Call call, Response response) throws IOException {
return result;
}
- public CompletableFuture waitForCompletion(RenderJob renderJob) {
+ public CompletableFuture waitForCompletion(RenderJob renderJob, long timeout,
+ TimeUnit unit) {
if (renderJob.getSpp() >= renderJob.getTargetSpp()) {
// job is already completed
return CompletableFuture.completedFuture(renderJob);
}
+ final long then = System.currentTimeMillis();
CompletableFuture completedJob = new CompletableFuture<>();
new Thread(() -> {
RenderJob current = renderJob;
try {
while (current.getSpp() < current.getTargetSpp()) {
- Thread.sleep(10_000);
+ if (then + unit.toMillis(timeout) < System.currentTimeMillis()) {
+ completedJob
+ .completeExceptionally(
+ new RenderException("Timeout after " + unit.toMillis(timeout) + " ms"));
+ return;
+ }
+ Thread.sleep(500);
current = getJob(current.getId()).get();
}
completedJob.complete(current);
@@ -184,14 +348,23 @@ public void onFailure(Call call, IOException e) {
@Override
public void onResponse(Call call, Response response) {
- if (response.code() == 200) {
- try (InputStreamReader reader = new InputStreamReader(response.body().byteStream())) {
- result.complete(gson.fromJson(reader, RenderJob.class));
- } catch (IOException e) {
- result.completeExceptionally(e);
+ try {
+ if (response.code() == 200) {
+ try (
+ ResponseBody body = response.body();
+ Reader reader = body.charStream()
+ ) {
+ result.complete(gson.fromJson(reader, RenderJob.class));
+ } catch (IOException e) {
+ result.completeExceptionally(e);
+ }
+ } else {
+ result.completeExceptionally(new IOException(
+ "The job could not be downloaded " + response.code() + " " + response
+ .message()));
}
- } else {
- result.completeExceptionally(new IOException("The job could not be downloaded"));
+ } finally {
+ response.close();
}
}
});
@@ -199,6 +372,35 @@ public void onResponse(Call call, Response response) {
return result;
}
+ public CompletableFuture cancelJob(String jobId) {
+ CompletableFuture result = new CompletableFuture<>();
+ client.newCall(new Request.Builder()
+ .url(baseUrl + "/jobs/" + jobId)
+ .patch(new MultipartBody.Builder().addFormDataPart("action", "cancel").build()).build())
+ .enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ result.completeExceptionally(e);
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ try {
+ if (response.code() == 204) {
+ result.complete(null);
+ } else {
+ result.completeExceptionally(new IOException(
+ "The job could not be downloaded " + response.code() + " " + response
+ .message()));
+ }
+ } finally {
+ response.close();
+ }
+ }
+ });
+ return result;
+ }
+
private static RequestBody fileBody(final File file, Supplier taskCreator) {
TaskTracker.Task task = taskCreator.get();
return new RequestBody() {
@@ -214,19 +416,13 @@ public long contentLength() {
@Override
public void writeTo(BufferedSink bufferedSink) throws IOException {
- Source source = null;
- try {
- source = Okio.source(file);
- //sink.writeAll(source);
- Buffer buf = new Buffer();
+ try (Source source = Okio.source(file)) {
long read = 0;
- for (long readCount; (readCount = source.read(buf, 2048)) != -1; ) {
- bufferedSink.write(buf, readCount);
+ for (long readCount; (readCount = source.read(bufferedSink.buffer(), 2048)) != -1; ) {
read += readCount;
+ bufferedSink.flush();
task.update((int) contentLength(), (int) read);
}
- } catch (Exception e) {
- e.printStackTrace();
}
task.close();
}
@@ -256,6 +452,12 @@ public void writeTo(BufferedSink bufferedSink) throws IOException {
}
public BufferedImage getPicture(String id) throws IOException {
- return ImageIO.read(new URL(baseUrl + "/jobs/" + id + "/latest.png"));
+ BufferedImage image = ImageIO.read(new URL(baseUrl + "/jobs/" + id + "/latest.png"));
+ BufferedImage img = new BufferedImage(image.getWidth(), image.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
+ Graphics g = img.getGraphics();
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+ return img;
}
}
diff --git a/src/main/java/de/lemaik/chunkymap/rendering/rs/RemoteRenderer.java b/src/main/java/de/lemaik/chunkymap/rendering/rs/RemoteRenderer.java
new file mode 100644
index 0000000..67111a7
--- /dev/null
+++ b/src/main/java/de/lemaik/chunkymap/rendering/rs/RemoteRenderer.java
@@ -0,0 +1,92 @@
+package de.lemaik.chunkymap.rendering.rs;
+
+import de.lemaik.chunkymap.rendering.FileBufferRenderContext;
+import de.lemaik.chunkymap.rendering.Renderer;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Paths;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import se.llbit.chunky.renderer.scene.Scene;
+import se.llbit.chunky.world.ChunkPosition;
+import se.llbit.chunky.world.RegionPosition;
+import se.llbit.util.ProgressListener;
+import se.llbit.util.TaskTracker;
+
+public class RemoteRenderer implements Renderer {
+
+ private final ApiClient api;
+ private final int samplesPerPixel;
+ private final String texturepack;
+ private final boolean initializeLocally;
+
+ public RemoteRenderer(String apiKey, int samplesPerPixel, String texturepack,
+ boolean initializeLocally) {
+ this.samplesPerPixel = samplesPerPixel;
+ this.texturepack = texturepack;
+ this.initializeLocally = initializeLocally;
+ this.api = new ApiClient("https://api.chunkycloud.lemaik.de", apiKey);
+ }
+
+ public boolean shouldInitializeLocally() {
+ return initializeLocally;
+ }
+
+ @Override
+ public CompletableFuture render(FileBufferRenderContext context, File[] texturepacks,
+ Consumer initializeScene) throws IOException {
+ Scene scene = context.getChunky().getSceneFactory().newScene();
+ initializeScene.accept(scene);
+
+ RenderJob job = null;
+ try {
+ if (initializeLocally) {
+ job = api
+ .createJob(context.getScene(), context.getOctree(), null, null,
+ this.texturepack, samplesPerPixel, new TaskTracker(ProgressListener.NONE)).get();
+ } else {
+ job = api.createJob(context.getScene(), scene.getChunks().stream().map(
+ ChunkPosition::getRegionPosition).collect(Collectors.toSet()).stream()
+ .map(position -> getRegionFile(scene, position))
+ .filter(File::exists)
+ .collect(Collectors.toList()), null, null,
+ this.texturepack, samplesPerPixel, new TaskTracker(ProgressListener.NONE)).get();
+ }
+ api.waitForCompletion(job, 10, TimeUnit.MINUTES).get();
+ return CompletableFuture.completedFuture(api.getPicture(job.getId()));
+ } catch (InterruptedException | ExecutionException e) {
+ if (job != null) {
+ try {
+ api.cancelJob(job.getId()).get();
+ } catch (InterruptedException | ExecutionException ignore) {
+ }
+ }
+ throw new IOException("Rendering failed", e);
+ }
+ }
+
+ private File getRegionFile(Scene scene, RegionPosition position) {
+ try {
+ Field worldPath = Scene.class.getDeclaredField("worldPath");
+ worldPath.setAccessible(true);
+ Field worldDimension = Scene.class.getDeclaredField("worldDimension");
+ worldDimension.setAccessible(true);
+ File world = new File((String) worldPath.get(scene));
+ int dimension = worldDimension.getInt(scene);
+ File dimWorld = dimension == 0 ? world : new File(world, "DIM" + dimension);
+ return Paths.get(dimWorld.getAbsolutePath(), "region", position.getMcaName()).toFile();
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException("Could not get region file", e);
+ }
+ }
+
+ @Override
+ public void setDefaultTexturepack(File texturepack) {
+ // no-op
+ }
+}
diff --git a/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java b/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java
index 17dccdd..3fa4943 100644
--- a/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java
+++ b/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java
@@ -4,12 +4,14 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
+import java.io.Reader;
import java.util.concurrent.CompletableFuture;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
+import okhttp3.ResponseBody;
/**
* A utility class to download Minecraft jars.
@@ -55,14 +57,23 @@ public void onFailure(Call call, IOException e) {
@Override
public void onResponse(Call call, Response response) throws IOException {
- JsonObject parsed = new JsonParser().parse(response.body().string()).getAsJsonObject();
- for (JsonElement versionData : parsed.getAsJsonArray("versions")) {
- if (versionData.getAsJsonObject().get("id").getAsString().equals(version)) {
- result.complete(versionData.getAsJsonObject().get("url").getAsString());
- return;
+ try (
+ ResponseBody body = response.body();
+ Reader reader = body.charStream()
+ ) {
+ JsonObject parsed = new JsonParser().parse(reader)
+ .getAsJsonObject();
+ for (JsonElement versionData : parsed.getAsJsonArray("versions")) {
+ if (versionData.getAsJsonObject().get("id").getAsString()
+ .equals(version)) {
+ result
+ .complete(versionData.getAsJsonObject().get("url").getAsString());
+ return;
+ }
}
+ result.completeExceptionally(
+ new Exception("Version " + version + " not found"));
}
- result.completeExceptionally(new Exception("Version " + version + " not found"));
}
});
@@ -81,9 +92,15 @@ public void onFailure(Call call, IOException e) {
@Override
public void onResponse(Call call, Response response) throws IOException {
- JsonObject parsed = new JsonParser().parse(response.body().string()).getAsJsonObject();
- result.complete(parsed.getAsJsonObject("downloads").getAsJsonObject("client").get("url")
- .getAsString());
+ try (
+ ResponseBody body = response.body();
+ Reader reader = body.charStream()
+ ) {
+ JsonObject parsed = new JsonParser().parse(reader).getAsJsonObject();
+ result.complete(
+ parsed.getAsJsonObject("downloads").getAsJsonObject("client").get("url")
+ .getAsString());
+ }
}
});
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 885d342..604bea0 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -2,3 +2,4 @@ name: ChunkyMap
version: ${project.version}
main: de.lemaik.chunkymap.ChunkyMapPlugin
loadbefore: [dynmap]
+softdepend: [dynmap]
diff --git a/vendor/chunky b/vendor/chunky
deleted file mode 160000
index 82f6ab1..0000000
--- a/vendor/chunky
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 82f6ab17654141e6628b4cb847115405b8bf61f3
diff --git a/vendor/chunky-denoiser b/vendor/chunky-denoiser
deleted file mode 160000
index 62b2471..0000000
--- a/vendor/chunky-denoiser
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 62b2471ca633857b2f5ac1fc4fe9e8161e21f667