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 ![banner](banner.png) +## 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