From fe07dfa8a1d868ec255ed6774a3eea02fa01244b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20=C3=96qvist?= Date: Thu, 9 May 2013 23:36:27 +0200 Subject: [PATCH] Added more sky rendering modes + Improved cloud rendering + Added more controls for sky parameters + Added gradient editor + Added color picker + HDRI textures fixes #91 (github) --- .gitignore | 1 + ChangeLog.txt | 12 +- .../llbit/chunky/renderer/scene/Camera.java | 11 +- .../chunky/renderer/scene/CameraPreset.java | 47 +- .../chunky/renderer/scene/PathTracer.java | 2 +- .../chunky/renderer/scene/RayTracer.java | 179 +++- .../se/llbit/chunky/renderer/scene/Scene.java | 43 +- .../renderer/scene/SceneDescription.java | 8 +- .../se/llbit/chunky/renderer/scene/Sky.java | 819 ++++++++++++++---- .../se/llbit/chunky/renderer/scene/Sun.java | 72 +- .../chunky/renderer/ui/ColorListener.java | 23 + .../llbit/chunky/renderer/ui/ColorPicker.java | 454 ++++++++++ .../chunky/renderer/ui/GradientEditor.java | 272 ++++++ .../chunky/renderer/ui/GradientListener.java | 27 + .../chunky/renderer/ui/GradientPicker.java | 144 +++ .../llbit/chunky/renderer/ui/GradientUI.java | 405 +++++++++ .../llbit/chunky/renderer/ui/HuePicker.java | 48 + .../chunky/renderer/ui/LightnessPicker.java | 52 ++ .../chunky/renderer/ui/RenderControls.java | 551 +++++++++--- .../chunky/renderer/ui/SaturationPicker.java | 53 ++ .../chunky/resources/AbstractHDRITexture.java | 84 ++ .../se/llbit/chunky/resources/HDRTexture.java | 179 ++++ .../se/llbit/chunky/resources/PFMTexture.java | 90 ++ .../se/llbit/chunky/resources/Texture.java | 43 +- .../src/java/se/llbit/chunky/ui/ChunkMap.java | 3 +- .../src/java/se/llbit/chunky/ui/Controls.java | 16 +- .../se/llbit/chunky/ui/TextInputDialog.java | 123 +++ .../se/llbit/chunky/ui/TextInputListener.java | 5 + .../se/llbit/chunky/ui/TextOutputDialog.java | 123 +++ .../src/java/se/llbit/chunky/world/Icon.java | 11 + .../se/llbit/chunky/world/SkymapTexture.java | 12 +- chunky/src/java/se/llbit/math/Color.java | 54 ++ chunky/src/java/se/llbit/math/Constants.java | 25 + chunky/src/java/se/llbit/math/Vector3d.java | 36 + chunky/src/java/se/llbit/math/Vector4d.java | 19 + chunky/src/res/Version.properties | 4 +- chunky/src/res/icons/camera.png | Bin 0 -> 557 bytes chunky/src/res/icons/colors.png | Bin 0 -> 525 bytes chunky/src/res/icons/load.png | Bin 0 -> 621 bytes chunky/src/res/icons/save.png | Bin 0 -> 660 bytes chunky/src/res/icons/sky.png | Bin 0 -> 432 bytes chunky/src/res/icons/skybox-back.png | Bin 0 -> 234 bytes chunky/src/res/icons/skybox-down.png | Bin 0 -> 238 bytes chunky/src/res/icons/skybox-front.png | Bin 0 -> 232 bytes chunky/src/res/icons/skybox-left.png | Bin 0 -> 233 bytes chunky/src/res/icons/skybox-right.png | Bin 0 -> 239 bytes chunky/src/res/icons/skybox-up.png | Bin 0 -> 238 bytes lib/jastadd/Json.jrag | 66 +- renderblocks.sh => misc/renderblocks.sh | 0 test.sh => misc/test.sh | 0 releasebot.py | 237 +++-- 51 files changed, 3840 insertions(+), 513 deletions(-) create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/ColorListener.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/ColorPicker.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/GradientEditor.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/GradientListener.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/GradientPicker.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/GradientUI.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/HuePicker.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/LightnessPicker.java create mode 100644 chunky/src/java/se/llbit/chunky/renderer/ui/SaturationPicker.java create mode 100644 chunky/src/java/se/llbit/chunky/resources/AbstractHDRITexture.java create mode 100644 chunky/src/java/se/llbit/chunky/resources/HDRTexture.java create mode 100644 chunky/src/java/se/llbit/chunky/resources/PFMTexture.java create mode 100644 chunky/src/java/se/llbit/chunky/ui/TextInputDialog.java create mode 100644 chunky/src/java/se/llbit/chunky/ui/TextInputListener.java create mode 100644 chunky/src/java/se/llbit/chunky/ui/TextOutputDialog.java create mode 100644 chunky/src/java/se/llbit/math/Constants.java create mode 100644 chunky/src/res/icons/camera.png create mode 100644 chunky/src/res/icons/colors.png create mode 100644 chunky/src/res/icons/load.png create mode 100644 chunky/src/res/icons/save.png create mode 100644 chunky/src/res/icons/sky.png create mode 100644 chunky/src/res/icons/skybox-back.png create mode 100644 chunky/src/res/icons/skybox-down.png create mode 100644 chunky/src/res/icons/skybox-front.png create mode 100644 chunky/src/res/icons/skybox-left.png create mode 100644 chunky/src/res/icons/skybox-right.png create mode 100644 chunky/src/res/icons/skybox-up.png rename renderblocks.sh => misc/renderblocks.sh (100%) rename test.sh => misc/test.sh (100%) diff --git a/.gitignore b/.gitignore index 042f36b08f..805b59c60c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ /latest.json /snapshot.json /snapshot +/credentials.json # IntelliJ IDEA Project files .idea/* diff --git a/ChangeLog.txt b/ChangeLog.txt index 22c25d6450..3e9780e2aa 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,4 +1,14 @@ -1.2.4 - TBD +1.3 - TBD + * Clouds can now be moved in all directoions + * HDRI sky textures can now be used, supported formats are: + + RGBE (.hdr) + + PFM + * Added new sky rendering modes: + + gradient + + spherical skymap + + skybox + * Added sky light parameter, affecting indirect sky lighting contribution + * The simulated sky has been modified (less pink, more bright) * Enable "Clear Selection" button after selecting region * Fixed regular glass blocks not letting light pass through (since 1.2.0). This could also affect other transparent blocks where the transparent diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java index 893916a8b5..58b6b82aed 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Camera.java @@ -860,11 +860,7 @@ public double getMaxFoV() { public JsonObject toJson() { JsonObject camera = new JsonObject(); - JsonObject position = new JsonObject(); - position.add("x", pos.x); - position.add("y", pos.y); - position.add("z", pos.z); - camera.add("position", position); + camera.add("position", pos.toJson()); JsonObject orientation = new JsonObject(); orientation.add("roll", roll); @@ -885,10 +881,7 @@ public JsonObject toJson() { @Override public void fromJson(JsonObject obj) { - JsonObject position = obj.get("position").object(); - pos.x = position.get("x").doubleValue(0); - pos.y = position.get("y").doubleValue(0); - pos.z = position.get("z").doubleValue(0); + pos.fromJson(obj.get("position").object()); JsonObject orientation = obj.get("orientation").object(); roll = orientation.get("roll").doubleValue(0); diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/CameraPreset.java b/chunky/src/java/se/llbit/chunky/renderer/scene/CameraPreset.java index 9ea9b1bf2a..439938d660 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/CameraPreset.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/CameraPreset.java @@ -26,7 +26,7 @@ */ abstract public class CameraPreset { - public static CameraPreset NONE = new CameraPreset("None") { + public static CameraPreset NONE = new CameraPreset("None", null) { @Override public void apply(Camera camera) { } @@ -36,31 +36,29 @@ public ImageIcon getIcon() { } }; public static CameraPreset ISO_WEST_NORTH = new Isometric("West-North", - Icon.isoWN.createIcon(), -Math.PI/4, -Math.PI/4); + Icon.isoWN.imageIcon(), -Math.PI/4, -Math.PI/4); public static CameraPreset ISO_NORTH_EAST = new Isometric("North-East", - Icon.isoNE.createIcon(), -3*Math.PI/4, -Math.PI/4); + Icon.isoNE.imageIcon(), -3*Math.PI/4, -Math.PI/4); public static CameraPreset ISO_EAST_SOUTH = new Isometric("East-South", - Icon.isoES.createIcon(), -5*Math.PI/4, -Math.PI/4); + Icon.isoES.imageIcon(), -5*Math.PI/4, -Math.PI/4); public static CameraPreset ISO_SOUTH_WEST = new Isometric("South-West", - Icon.isoSW.createIcon(), -7*Math.PI/4, -Math.PI/4); - public static CameraPreset SKYBOX_EAST = new Skybox("East", Math.PI, -Math.PI/2); - public static CameraPreset SKYBOX_WEST = new Skybox("West", 0, -Math.PI/2); - public static CameraPreset SKYBOX_UP = new Skybox("Up", -Math.PI/2, Math.PI); - public static CameraPreset SKYBOX_DOWN = new Skybox("Down", -Math.PI/2, 0); - public static CameraPreset SKYBOX_NORTH = new Skybox("North", -Math.PI/2, -Math.PI/2); - public static CameraPreset SKYBOX_SOUTH = new Skybox("South", Math.PI/2, -Math.PI/2); + Icon.isoSW.imageIcon(), -7*Math.PI/4, -Math.PI/4); + public static CameraPreset SKYBOX_RIGHT = new Skybox("Right", Icon.skyboxRight.imageIcon(), Math.PI, -Math.PI/2); + public static CameraPreset SKYBOX_LEFT = new Skybox("Left", Icon.skyboxLeft.imageIcon(), 0, -Math.PI/2); + public static CameraPreset SKYBOX_UP = new Skybox("Up", Icon.skyboxUp.imageIcon(), -Math.PI/2, Math.PI); + public static CameraPreset SKYBOX_DOWN = new Skybox("Down", Icon.skyboxDown.imageIcon(), -Math.PI/2, 0); + public static CameraPreset SKYBOX_FRONT = new Skybox("Front (North)", Icon.skyboxFront.imageIcon(), -Math.PI/2, -Math.PI/2); + public static CameraPreset SKYBOX_BACK = new Skybox("Back", Icon.skyboxBack.imageIcon(), Math.PI/2, -Math.PI/2); public static class Isometric extends CameraPreset { private final double yaw; private final double pitch; - private final ImageIcon icon; public Isometric(String name, ImageIcon icon, double yaw, double pitch) { - super("Isometric " + name); + super("Isometric " + name, icon); this.yaw = yaw; this.pitch = pitch; - this.icon = icon; } @Override @@ -68,22 +66,19 @@ public void apply(Camera camera) { camera.setView(yaw, pitch, 0); camera.setProjectionMode(ProjectionMode.PARALLEL); } - - @Override - public ImageIcon getIcon() { - return icon; - } } public static class Skybox extends CameraPreset { private final double yaw; private final double pitch; + private final ImageIcon icon; - public Skybox(String name, double yaw, double pitch) { - super("Skybox " + name); + public Skybox(String name, ImageIcon icon, double yaw, double pitch) { + super("Skybox " + name, icon); this.yaw = yaw; this.pitch = pitch; + this.icon = icon; } @Override @@ -95,14 +90,16 @@ public void apply(Camera camera) { @Override public ImageIcon getIcon() { - return null; + return icon; } } private final String name; + private ImageIcon icon; - public CameraPreset(String name) { + public CameraPreset(String name, ImageIcon icon) { this.name = name; + this.icon = icon; } @Override @@ -116,5 +113,7 @@ public String toString() { */ abstract public void apply(Camera camera); - abstract public ImageIcon getIcon(); + public ImageIcon getIcon() { + return icon; + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java index be37f3ad8f..ba0e0a0988 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java @@ -73,7 +73,7 @@ public static final void pathTrace(Scene scene, Ray ray, WorkerState state, // sky color scene.sky.getSkySpecularColor(ray, scene.waterHeight > 0); } else { - scene.sky.getSkyDiffuseColor(ray, scene.waterHeight > 0); + scene.sky.getSkyColor(ray, scene.waterHeight > 0); } break; } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/RayTracer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/RayTracer.java index 9a52f6c377..2e6ee6a980 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/RayTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/RayTracer.java @@ -1,4 +1,4 @@ -/* Copyright (c) 2013 Jesper Öqvist +/* Copyright (c) 2013-2014 Jesper Öqvist * * This file is part of Chunky. * @@ -20,7 +20,6 @@ import se.llbit.chunky.renderer.WorkerState; import se.llbit.chunky.world.Block; import se.llbit.chunky.world.Clouds; -import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Ray.RayPool; @@ -28,6 +27,8 @@ * @author Jesper Öqvist */ public class RayTracer { + private static final double CLOUD_OPACITY = 0.9; + /** * @param scene * @param state @@ -70,7 +71,7 @@ public static void quickTrace(Scene scene, WorkerState state) { */ public static boolean nextIntersection(Scene scene, Ray ray, WorkerState state) { - if (scene.cloudsEnabled && cloudIntersection(scene, ray)) { + if (scene.sky().cloudsEnabled() && cloudIntersection(scene, ray)) { Ray oct = state.rayPool.get(ray); if (nextWorldIntersection(scene, oct, state.rayPool) && oct.distance <= ray.distance) { @@ -81,12 +82,9 @@ public static boolean nextIntersection(Scene scene, Ray ray, WorkerState state) ray.prevMaterial = oct.prevMaterial; ray.currentMaterial = oct.currentMaterial; } else { - ray.color.set(1, 1, 1, 1); ray.prevMaterial = ray.currentMaterial; ray.currentMaterial = Block.GRASS_ID; - ray.x.scaleAdd(ray.tNear, ray.d, ray.x); - ray.n.set(0, -QuickMath.signum(ray.d.y), 0); - ray.distance += ray.tNear; + ray.x.scaleAdd(ray.tNear + Ray.EPSILON, ray.d); } state.rayPool.dispose(oct); return true; @@ -130,18 +128,167 @@ private static boolean nextWorldIntersection(Scene scene, Ray ray, } private static boolean cloudIntersection(Scene scene, Ray ray) { - if (ray.d.y != 0) { - ray.t = (scene.cloudHeight - scene.origin.y - ray.x.y) / ray.d.y; - if (ray.t > Ray.EPSILON) { - double u = ray.x.x + ray.d.x * ray.t; - double v = ray.x.z + ray.d.z * ray.t; - if (Clouds.getCloud((int) (u/128), (int) (v/128)) != 0) { - ray.distance += ray.t; - return true; + double offsetX = scene.sky().cloudXOffset(); + double offsetY = scene.sky().cloudYOffset(); + double offsetZ = scene.sky().cloudZOffset(); + double inv_size = 1/scene.sky().cloudSize(); + double cloudBot = offsetY - scene.origin.y; + double cloudTop = offsetY - scene.origin.y + 5; + int target = 1; + double t_offset = 0; + ray.tNear = Double.POSITIVE_INFINITY; + if (ray.x.y < cloudBot || ray.x.y > cloudTop) { + if (ray.d.y > 0) { + t_offset = (cloudBot - ray.x.y) / ray.d.y; + } else { + t_offset = (cloudTop - ray.x.y) / ray.d.y; + } + if (t_offset < 0) { + return false; + } + // ray is entering cloud + if (inCloud((ray.d.x*t_offset + ray.x.x)*inv_size + offsetX, (ray.d.z*t_offset + ray.x.z)*inv_size + offsetZ)) { + ray.tNear = t_offset; + ray.distance += t_offset; + ray.n.set(0, -Math.signum(ray.d.y), 0); + ray.color.set(1,1,1,CLOUD_OPACITY); + return true; + } + } else if (inCloud(ray.x.x*inv_size + offsetX, ray.x.z*inv_size + offsetZ)) { + target = 0; + return false; + } + double tExit = Double.MAX_VALUE; + if (ray.d.y > 0) { + tExit = (cloudTop - ray.x.y) / ray.d.y - t_offset; + } else { + tExit = (cloudBot - ray.x.y) / ray.d.y - t_offset; + } + double x0 = (ray.x.x + ray.d.x*t_offset)*inv_size + offsetX; + double z0 = (ray.x.z + ray.d.z*t_offset)*inv_size + offsetZ; + double xp = x0; + double zp = z0; + int ix = (int) Math.floor(xp); + int iz = (int) Math.floor(zp); + int xmod = (int)Math.signum(ray.d.x), zmod = (int)Math.signum(ray.d.z); + double dx = Math.abs(ray.d.x)*inv_size; + double dz = Math.abs(ray.d.z)*inv_size; + double t = 0; + int i = 0; + int nx = 0, nz = 0; + if (dx > dz) { + double m = dz/dx; + double xrem = xmod * (ix+0.5*(1+xmod) - xp); + double zlimit = xrem*m; + while (t < tExit) { + double zrem = zmod * (iz+0.5*(1+zmod) - zp); + zp = z0 + zmod * (i+1) * m; + if (zrem < zlimit) { + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i/dx + zrem/dz; + nx = 0; + nz = -zmod; + break; + } + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i+xrem)/dx; + nx = -xmod; + nz = 0; + break; + } + } else { + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i+xrem)/dx; + nx = -xmod; + nz = 0; + break; + } + if (zrem <= m) { + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i/dx + zrem/dz; + nx = 0; + nz = -zmod; + break; + } + } + } + t = i/dx; + i+=1; + } + } else { + double m = dx/dz; + double zrem = zmod * (iz+0.5*(1+zmod) - zp); + double xlimit = zrem*m; + while (t < tExit) { + double xrem = xmod * (ix+0.5*(1+xmod) - xp); + xp = x0 + xmod * (i+1) * m; + if (xrem < xlimit) { + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i/dz + xrem/dx; + nx = -xmod; + nz = 0; + break; + } + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i+zrem)/dz; + nx = 0; + nz = -zmod; + break; + } + } else { + iz += zmod; + if (Clouds.getCloud(ix, iz) == target) { + t = (i+zrem)/dz; + nx = 0; + nz = -zmod; + break; + } + if (xrem <= m) { + ix += xmod; + if (Clouds.getCloud(ix, iz) == target) { + t = i/dz + xrem/dx; + nx = -xmod; + nz = 0; + break; + } + } } + t = i/dz; + i+=1; + } + } + int ny = 0; + if (target == 1) { + if (t > tExit) { + return false; + } + } else { + if (t > tExit) { + nx = 0; + ny = (int) Math.signum(ray.d.y); + nz = 0; + t = tExit; + } else { + nx = -nx; + nz = -nz; } } - return false; + ray.n.set(nx, ny, nz); + ray.tNear = t + t_offset; + ray.distance += ray.tNear; + ray.color.set(1, 1, 1, CLOUD_OPACITY); + return true; } + private static boolean inCloud(double x, double z) { + return Clouds.getCloud((int)Math.floor(x), (int)Math.floor(z)) == 1; + } + + } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index 136e6e0912..f6de5da094 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -149,8 +149,6 @@ public class Scene extends SceneDescription { */ public static final double DEFAULT_EXPOSURE = 1.0; - protected static final int DEFAULT_CLOUD_HEIGHT = 128; - protected double waterVisibility = DEFAULT_WATER_VISIBILITY; /** @@ -249,8 +247,6 @@ public void set(Scene other) { clearWater = other.clearWater; biomeColors = other.biomeColors; sunEnabled = other.sunEnabled; - cloudsEnabled = other.cloudsEnabled; - cloudHeight = other.cloudHeight; emittersEnabled = other.emittersEnabled; emitterIntensity = other.emitterIntensity; atmosphereEnabled = other.atmosphereEnabled; @@ -326,7 +322,7 @@ public synchronized void loadScene( loadDescription(context.getSceneDescriptionInputStream(sceneName)); // load the configured skymap file - sky.loadSkyMap(); + sky.loadSkymap(); if (sdfVersion < SDF_VERSION) { logger.warn("Old scene version detected! The scene may not have loaded correctly."); @@ -1793,43 +1789,6 @@ public float[] getGrassColor(int x, int z) { } } - /** - * @return true if cloud rendering is enabled - */ - public boolean cloudsEnabled() { - return cloudsEnabled; - } - - /** - * Enable/disable clouds rendering - * @param value - */ - public void setCloudsEnabled(boolean value) { - if (value != cloudsEnabled) { - cloudsEnabled = value; - refresh(); - } - } - - /** - * Change the cloud height - * @param value - */ - public void setCloudHeight(int value) { - if (value != cloudHeight) { - cloudHeight = value; - if (cloudsEnabled) - refresh(); - } - } - - /** - * @return The current cloud height - */ - public int getCloudHeight() { - return cloudHeight; - } - /** * Merge a render dump into this scene * @param dumpFile diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SceneDescription.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SceneDescription.java index c1049ac374..b13d085dc6 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/SceneDescription.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/SceneDescription.java @@ -48,7 +48,7 @@ public class SceneDescription implements Refreshable, JSONifiable { /** * The current Scene Description Format (SDF) version */ - public static final int SDF_VERSION = 2; + public static final int SDF_VERSION = 3; public int sdfVersion = -1; public String name = "default"; @@ -98,8 +98,6 @@ public class SceneDescription implements Refreshable, JSONifiable { protected boolean emittersEnabled = true; protected double emitterIntensity = Scene.DEFAULT_EMITTER_INTENSITY; protected boolean sunEnabled = true; - protected boolean cloudsEnabled = false; - protected int cloudHeight = Scene.DEFAULT_CLOUD_HEIGHT; protected boolean stillWater = false; protected boolean clearWater = false; protected boolean biomeColors = true; @@ -157,8 +155,6 @@ public synchronized JsonObject toJson() { desc.add("emittersEnabled", emittersEnabled); desc.add("emitterIntensity", emitterIntensity); desc.add("sunEnabled", sunEnabled); - desc.add("cloudsEnabled", cloudsEnabled); - desc.add("cloudHeight", cloudHeight); desc.add("stillWater", stillWater); desc.add("clearWater", clearWater); desc.add("biomeColorsEnabled", biomeColors); @@ -211,8 +207,6 @@ public synchronized void fromJson(JsonObject desc) { emittersEnabled = desc.get("emittersEnabled").boolValue(true); emitterIntensity = desc.get("emitterIntensity").doubleValue(Scene.DEFAULT_EMITTER_INTENSITY); sunEnabled = desc.get("sunEnabled").boolValue(true); - cloudsEnabled = desc.get("cloudsEnabled").boolValue(false); - cloudHeight = desc.get("cloudHeight").intValue(Scene.DEFAULT_CLOUD_HEIGHT); stillWater = desc.get("stillWater").boolValue(false); clearWater = desc.get("clearWater").boolValue(false); biomeColors = desc.get("biomeColorsEnabled").boolValue(true); diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java index 13d5ab0af8..af22dffaa6 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Sky.java @@ -1,4 +1,4 @@ -/* Copyright (c) 2012-2013 Jesper Öqvist +/* Copyright (c) 2012-2014 Jesper Öqvist * * This file is part of Chunky. * @@ -16,23 +16,34 @@ */ package se.llbit.chunky.renderer.scene; -import java.awt.Color; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; import javax.imageio.ImageIO; import org.apache.commons.math3.util.FastMath; import org.apache.log4j.Logger; -import se.llbit.chunky.PersistentSettings; +import se.llbit.chunky.resources.HDRTexture; +import se.llbit.chunky.resources.PFMTexture; import se.llbit.chunky.resources.Texture; import se.llbit.chunky.world.SkymapTexture; +import se.llbit.json.JsonArray; +import se.llbit.json.JsonNull; import se.llbit.json.JsonObject; +import se.llbit.json.JsonValue; +import se.llbit.math.Color; +import se.llbit.math.Constants; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3d; +import se.llbit.math.Vector4d; import se.llbit.util.JSONifiable; +import se.llbit.util.NotNull; /** * Sky model for ray tracing @@ -40,6 +51,35 @@ */ public class Sky implements JSONifiable { + /** + * Default sky light intensity + */ + public static final double DEFAULT_INTENSITY = 1; + + /** + * Default cloud y-position + */ + protected static final int DEFAULT_CLOUD_HEIGHT = 128; + + protected static final int DEFAULT_CLOUD_SIZE = 64; + + /** + * Maximum sky light intensity + */ + public static final double MAX_INTENSITY = 50; + + /** + * Minimum sky light intensity + */ + public static final double MIN_INTENSITY = 0.01; + + public static final int SKYBOX_UP = 0; + public static final int SKYBOX_DOWN = 1; + public static final int SKYBOX_FRONT = 2; + public static final int SKYBOX_BACK = 3; + public static final int SKYBOX_RIGHT = 4; + public static final int SKYBOX_LEFT = 5; + /** * Sky rendering mode * @author Jesper Öqvist @@ -49,14 +89,23 @@ public enum SkyMode { * Use simulated sky */ SIMULATED("Simulated"), + // TODO + ///** + // * Simulated night-time + // */ + //SIMULATED_NIGHT("Simulated (night)"), + /** + * Use a gradient + */ + GRADIENT("Color Gradient"), /** * Use a panormaic skymap */ - SKYMAP("Panoramic Skymap (above horizon)"), + SKYMAP_PANORAMIC("Skymap (panoramic)"), /** - * Use a gradient + * Light probe */ - GRADIENT("Color Gradient"), + SKYMAP_SPHERICAL("Skymap (spherical)"), /** * Use a skybox */ @@ -72,39 +121,77 @@ public enum SkyMode { public String toString() { return name; } + + public static final SkyMode DEFAULT = SIMULATED; + public static final SkyMode[] values = values(); + + public static SkyMode get(String name) { + for (SkyMode mode: values) { + if (mode.name().equals(name)) { + return mode; + } + } + return DEFAULT; + } + }; private static final Logger logger = Logger.getLogger(Sky.class); - private Texture skymap = null; + @NotNull + private Texture skymap = Texture.EMPTY_TEXTURE; + private final Texture skybox[] = { + Texture.EMPTY_TEXTURE, Texture.EMPTY_TEXTURE, + Texture.EMPTY_TEXTURE, Texture.EMPTY_TEXTURE, + Texture.EMPTY_TEXTURE, Texture.EMPTY_TEXTURE }; private String skymapFileName = ""; + private final String skyboxFileName[] = {"", "", "", "", "", ""}; private final SceneDescription scene; private double rotation = 0; private boolean mirrored = true; + private double horizonOffset = 0.1; + private boolean cloudsEnabled = false; + private double cloudSize = DEFAULT_CLOUD_SIZE; + private final Vector3d cloudOffset = new Vector3d(0, DEFAULT_CLOUD_HEIGHT, 0); + + private double skyLightModifier = DEFAULT_INTENSITY; - // final to ensure that we don't do a lot of redundant re-allocation - private final Vector3d groundColor = new Vector3d(0, 0, 1); + private List gradient = new LinkedList(); /** * Current rendering mode */ - private SkyMode mode = SkyMode.SIMULATED; + private SkyMode mode = SkyMode.DEFAULT; /** * @param sceneDescription */ public Sky(SceneDescription sceneDescription) { this.scene = sceneDescription; + makeDefaultGradient(gradient); } /** * Load the configured skymap file * @param fileName */ - public void loadSkyMap() { - if (!skymapFileName.isEmpty()) { - loadSkyMap(skymapFileName); + public void loadSkymap() { + switch (mode) { + case SKYMAP_PANORAMIC: + case SKYMAP_SPHERICAL: + if (!skymapFileName.isEmpty()) { + loadSkymap(skymapFileName); + } + break; + case SKYBOX: + for (int i = 0; i < 6; ++i) { + if (!skyboxFileName[i].isEmpty()) { + loadSkyboxTexture(skyboxFileName[i], i); + } + } + default: + break; } } @@ -112,21 +199,9 @@ public void loadSkyMap() { * Load a panoramic skymap texture * @param fileName */ - public void loadSkyMap(String fileName) { + public void loadSkymap(String fileName) { skymapFileName = fileName; - File sky = new File(skymapFileName); - if (sky.exists()) { - try { - logger.info("Loading sky map: " + fileName); - skymap = new SkymapTexture(ImageIO.read(sky)); - } catch (IOException e) { - logger.warn("Could not load skymap: " + fileName); - } catch (Throwable e) { - logger.error("Unexpected exception ocurred!", e); - } - } else { - logger.warn("Skymap could not be opened: " + fileName); - } + skymap = loadSkyTexture(fileName, skymap); scene.refresh(); } @@ -135,21 +210,150 @@ public void loadSkyMap(String fileName) { * @param other */ public void set(Sky other) { + horizonOffset = other.horizonOffset; + cloudsEnabled = other.cloudsEnabled; + cloudOffset.set(other.cloudOffset); + cloudSize = other.cloudSize; skymapFileName = other.skymapFileName; skymap = other.skymap; rotation = other.rotation; mirrored = other.mirrored; - groundColor.set(other.groundColor); + skyLightModifier = other.skyLightModifier; + gradient = new ArrayList(other.gradient); + mode = other.mode; + for (int i = 0; i < 6; ++i) { + skybox[i] = other.skybox[i]; + skyboxFileName[i] = other.skyboxFileName[i]; + } } /** - * Unload the skymap texture and use the default sky instead + * Calculate sky color for the ray, based on sky mode + * @param ray + * @param blackBelowHorizon */ - public synchronized void unloadSkymap() { - skymapFileName = ""; - skymap = null; - PersistentSettings.removeSetting("skymap"); - scene.refresh(); + public void getSkyDiffuseColorInner(Ray ray, boolean blackBelowHorizon) { + switch (mode) { + case GRADIENT: + { + double angle = Math.asin(ray.d.y); + int x = 0; + if (gradient.size() > 1) { + double pos = (angle+Constants.HALF_PI)/Math.PI; + Vector4d c0 = gradient.get(x); + Vector4d c1 = gradient.get(x+1); + double xx = (pos - c0.w) / (c1.w-c0.w); + while (x+2 < gradient.size() && xx > 1) { + x += 1; + c0 = gradient.get(x); + c1 = gradient.get(x+1); + xx = (pos - c0.w) / (c1.w-c0.w); + } + xx = 0.5*(Math.sin(Math.PI*xx-Constants.HALF_PI)+1); + double a = 1-xx; + double b = xx; + ray.color.set(a*c0.x+b*c1.x, a*c0.y+b*c1.y, a*c0.z+b*c1.z, 1); + } + break; + } + case SIMULATED: + { + scene.sun().calcSkyLight(ray, horizonOffset); + break; + } + case SKYMAP_PANORAMIC: + { + if (mirrored) { + double theta = FastMath.atan2(ray.d.z, ray.d.x); + theta += rotation; + theta /= Constants.TAU; + if (theta > 1 || theta < 0) { + theta = (theta%1 + 1) % 1; + } + double phi = Math.abs(Math.asin(ray.d.y)) / Constants.HALF_PI; + skymap.getColor(theta, phi, ray.color); + } else { + double theta = FastMath.atan2(ray.d.z, ray.d.x); + theta += rotation; + theta /= Constants.TAU; + theta = (theta%1 + 1) % 1; + double phi = (Math.asin(ray.d.y) + Constants.HALF_PI) / Math.PI; + skymap.getColor(theta, phi, ray.color); + } + break; + } + case SKYMAP_SPHERICAL: + { + double cos = FastMath.cos(-rotation); + double sin = FastMath.sin(-rotation); + double x = cos*ray.d.x + sin*ray.d.z; + double y = ray.d.y; + double z = -sin*ray.d.x + cos*ray.d.z; + double len = Math.sqrt(x*x + y*y); + double theta = (len < Ray.EPSILON) ? 0 : Math.acos(-z)/Constants.TAU; + double u = theta*x + .5; + double v = .5 + theta*y; + skymap.getColor(u, v, ray.color); + break; + } + case SKYBOX: + { + double cos = FastMath.cos(-rotation); + double sin = FastMath.sin(-rotation); + double x = cos*ray.d.x + sin*ray.d.z; + double y = ray.d.y; + double z = -sin*ray.d.x + cos*ray.d.z; + double xabs = QuickMath.abs(x); + double yabs = QuickMath.abs(y); + double zabs = QuickMath.abs(z); + if (y > xabs && y > zabs) { + double alpha = 1 / yabs; + skybox[SKYBOX_UP].getColor( + (1 + x*alpha)/2.0, + (1 + z*alpha)/2.0, + ray.color); + } + else if (-z > xabs && -z > yabs) { + double alpha = 1 / zabs; + skybox[SKYBOX_FRONT].getColor( + (1 + x*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (z > xabs && z > yabs) { + double alpha = 1 / zabs; + skybox[SKYBOX_BACK].getColor( + (1 - x*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (-x > zabs && -x > yabs) { + double alpha = 1 / xabs; + skybox[SKYBOX_LEFT].getColor( + (1 - z*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (x > zabs && x > yabs) { + double alpha = 1 / xabs; + skybox[SKYBOX_RIGHT].getColor( + (1 + z*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (-y > xabs && -y > zabs) { + double alpha = 1 / yabs; + skybox[SKYBOX_DOWN].getColor( + (1 + x*alpha)/2.0, + (1 - z*alpha)/2.0, + ray.color); + } + break; + } + default: + break; + } + ray.hit = true; } /** @@ -157,42 +361,10 @@ public synchronized void unloadSkymap() { * @param ray * @param blackBelowHorizon */ - public void getSkyDiffuseColor(Ray ray, boolean blackBelowHorizon) { - if (getGroundColor(ray, blackBelowHorizon)) { - return; - } else if (skymap == null) { - scene.sun().skylight(ray); - ray.hit = true; - return; - } - double r = ray.d.z * ray.d.z + ray.d.x * ray.d.x; - double theta = 0; - if (r > Ray.EPSILON) - theta = FastMath.asin(ray.d.z / FastMath.sqrt(r)); - if (ray.d.x < 0) - theta = Math.PI - theta; - theta += rotation; - if (theta > 2 * Math.PI || theta < 0) { - theta = theta % (2 * Math.PI); - if (theta < 0) - theta += 2 * Math.PI; - } - double phi = QuickMath.abs(FastMath.asin(ray.d.y)); - skymap.getColor(theta / (2*Math.PI), (2 * phi / Math.PI), ray.color); - ray.hit = true; - } - - private boolean getGroundColor(Ray ray, boolean blackBelowHorizon) { - if (blackBelowHorizon && ray.d.y < 0) { - ray.color.set(0, 0, 0, 1); - ray.hit = true; - return true; - } else if (!mirrored && ray.d.y < 0) { - ray.color.set(groundColor.x, groundColor.y, groundColor.z, 1); - ray.hit = true; - return true; - } - return false; + public void getSkyColor(Ray ray, boolean blackBelowHorizon) { + getSkyDiffuseColorInner(ray, blackBelowHorizon); + ray.color.scale(skyLightModifier); + ray.color.w = 1; } /** @@ -201,47 +373,105 @@ private boolean getGroundColor(Ray ray, boolean blackBelowHorizon) { * @param blackBelowHorizon */ public void getSkyColorInterpolated(Ray ray, boolean blackBelowHorizon) { - if (getGroundColor(ray, blackBelowHorizon)) { - return; - - } else if (scene.sunEnabled && scene.sun().intersect(ray)) { - double r = ray.color.x; - double g = ray.color.y; - double b = ray.color.z; - getPanoramaColorInterpolated(ray); - ray.color.x = ray.color.x + r; - ray.color.y = ray.color.y + g; - ray.color.z = ray.color.z + b; - - } else { - - getPanoramaColorInterpolated(ray); + switch (mode) { + case SKYMAP_PANORAMIC: + { + if (mirrored) { + double theta = FastMath.atan2(ray.d.z, ray.d.x); + theta += rotation; + theta /= Constants.TAU; + theta = (theta%1 + 1) % 1; + double phi = Math.abs(Math.asin(ray.d.y)) / Constants.HALF_PI; + skymap.getColorInterpolated(theta, phi, ray.color); + } else { + double theta = FastMath.atan2(ray.d.z, ray.d.x); + theta += rotation; + theta /= Constants.TAU; + if (theta > 1 || theta < 0) { + theta = (theta%1 + 1) % 1; + } + double phi = (Math.asin(ray.d.y) + Constants.HALF_PI) / Math.PI; + skymap.getColorInterpolated(theta, phi, ray.color); + } + break; } - ray.hit = true; - } - - private void getPanoramaColorInterpolated(Ray ray) { - if (skymap == null) { - scene.sun().skylight(ray); - } else { - double r = ray.d.z * ray.d.z + ray.d.x * ray.d.x; - double theta = 0; - if (r > Ray.EPSILON) - theta = FastMath.asin(ray.d.z / FastMath.sqrt(r)); - if (ray.d.x < 0) - theta = Math.PI - theta; - theta += rotation; - if (theta > 2 * Math.PI || theta < 0) { - theta = theta % (2 * Math.PI); - if (theta < 0) - theta += 2 * Math.PI; + case SKYMAP_SPHERICAL: + { + double cos = FastMath.cos(-rotation); + double sin = FastMath.sin(-rotation); + double x = cos*ray.d.x + sin*ray.d.z; + double y = ray.d.y; + double z = -sin*ray.d.x + cos*ray.d.z; + double len = Math.sqrt(x*x + y*y); + double theta = (len < Ray.EPSILON) ? 0 : Math.acos(-z)/Constants.TAU; + double u = theta*x + .5; + double v = .5 + theta*y; + skymap.getColorInterpolated(u, v, ray.color); + break; + } + case SKYBOX: + { + double cos = FastMath.cos(-rotation); + double sin = FastMath.sin(-rotation); + double x = cos*ray.d.x + sin*ray.d.z; + double y = ray.d.y; + double z = -sin*ray.d.x + cos*ray.d.z; + double xabs = QuickMath.abs(x); + double yabs = QuickMath.abs(y); + double zabs = QuickMath.abs(z); + if (y > xabs && y > zabs) { + double alpha = 1 / yabs; + skybox[SKYBOX_UP].getColorInterpolated( + (1 + x*alpha)/2.0, + (1 + z*alpha)/2.0, + ray.color); } - double phi = QuickMath.abs(FastMath.asin(ray.d.y)); - theta /= 2 * Math.PI; - phi /= Math.PI / 2; - phi = 1 - phi; - skymap.getColorInterpolated(theta, phi, ray.color); + else if (-z > xabs && -z > yabs) { + double alpha = 1 / zabs; + skybox[SKYBOX_FRONT].getColorInterpolated( + (1 + x*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (z > xabs && z > yabs) { + double alpha = 1 / zabs; + skybox[SKYBOX_BACK].getColorInterpolated( + (1 - x*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (-x > zabs && -x > yabs) { + double alpha = 1 / xabs; + skybox[SKYBOX_LEFT].getColorInterpolated( + (1 - z*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (x > zabs && x > yabs) { + double alpha = 1 / xabs; + skybox[SKYBOX_RIGHT].getColorInterpolated( + (1 + z*alpha)/2.0, + (1 + y*alpha)/2.0, + ray.color); + } + else if (-y > xabs && -y > zabs) { + double alpha = 1 / yabs; + skybox[SKYBOX_DOWN].getColorInterpolated( + (1 + x*alpha)/2.0, + (1 - z*alpha)/2.0, + ray.color); + } + break; + } + default: + getSkyDiffuseColorInner(ray, blackBelowHorizon); } + if (scene.sunEnabled) { + addSunColor(ray); + } + ray.hit = true; + //ray.color.scale(skyLightModifier); + ray.color.w = 1; } /** @@ -250,18 +480,26 @@ private void getPanoramaColorInterpolated(Ray ray) { * @param blackBelowHorizon */ public void getSkySpecularColor(Ray ray, boolean blackBelowHorizon) { - if (scene.sunEnabled && scene.sun().intersect(ray)) { - double r = ray.color.x; - double g = ray.color.y; - double b = ray.color.z; - getSkyDiffuseColor(ray, blackBelowHorizon); + getSkyColor(ray, blackBelowHorizon); + if (scene.sunEnabled) { + addSunColor(ray); + } + } + + /** + * Add sun color contribution. This does not alpha blend the sun color + * because the Minecraft sun texture has no alpha channel. + * @param ray + */ + private void addSunColor(Ray ray) { + double r = ray.color.x; + double g = ray.color.y; + double b = ray.color.z; + if (scene.sun().intersect(ray)) { + // blend sun color with current color ray.color.x = ray.color.x + r; ray.color.y = ray.color.y + g; ray.color.z = ray.color.z + b; - ray.hit = true; - - } else { - getSkyDiffuseColor(ray, blackBelowHorizon); } } @@ -299,33 +537,25 @@ public boolean isMirrored() { return mirrored; } - /** - * @return The current ground color - */ - public Color getGroundColor() { - return new Color( - (float) QuickMath.min(1, groundColor.x), - (float) QuickMath.min(1, groundColor.y), - (float) QuickMath.min(1, groundColor.z)); - } - - /** - * Set a new ground color - * @param color - */ - public void setGroundColor(Color color) { - groundColor.x = FastMath.pow(color.getRed() / 255., Scene.DEFAULT_GAMMA); - groundColor.y = FastMath.pow(color.getGreen() / 255., Scene.DEFAULT_GAMMA); - groundColor.z = FastMath.pow(color.getBlue() / 255., Scene.DEFAULT_GAMMA); - scene.refresh(); - } - /** * Set the sky rendering mode - * @param mode + * @param newMode */ - public void setSkyMode(SkyMode mode) { - this.mode = mode; + public void setSkyMode(SkyMode newMode) { + if (this.mode != newMode) { + this.mode = newMode; + if (newMode != SkyMode.SKYMAP_PANORAMIC && newMode != SkyMode.SKYMAP_SPHERICAL) { + skymapFileName = ""; + skymap = Texture.EMPTY_TEXTURE; + } + if (newMode != SkyMode.SKYBOX) { + for (int i = 0; i < 6; ++i) { + skybox[i] = Texture.EMPTY_TEXTURE; + skyboxFileName[i] = ""; + } + } + scene.refresh(); + } } /** @@ -338,31 +568,302 @@ public SkyMode getSkyMode() { @Override public JsonObject toJson() { JsonObject sky = new JsonObject(); - if (skymap != null) { - sky.add("skymap", skymapFileName); - } sky.add("skyYaw", rotation); sky.add("skyMirrored", mirrored); - JsonObject groundColorObj = new JsonObject(); - groundColorObj.add("red", groundColor.x); - groundColorObj.add("green", groundColor.y); - groundColorObj.add("blue", groundColor.z); - sky.add("groundColor", groundColorObj); + sky.add("skyLight", skyLightModifier); + sky.add("mode", mode.name()); + sky.add("horizonOffset", horizonOffset); + sky.add("cloudsEnabled", cloudsEnabled); + sky.add("cloudSize", cloudSize); + sky.add("cloudOffset", cloudOffset.toJson()); + + // always save gradient + sky.add("gradient", gradientJson(gradient)); + + switch (mode) { + case SKYMAP_PANORAMIC: + case SKYMAP_SPHERICAL: + { + if (!skymap.isEmptyTexture()) { + sky.add("skymap", skymapFileName); + } + break; + } + case SKYBOX: + { + JsonArray array = new JsonArray(); + for (int i = 0; i < 6; ++i) { + if (!skybox[i].isEmptyTexture()) { + array.add(skyboxFileName[i]); + } else { + array.add(new JsonNull()); + } + } + sky.add("skybox", array); + break; + } + default: + break; + } return sky; } @Override - public void fromJson(JsonObject obj) { - skymapFileName = obj.get("skymap").stringValue(""); - if (skymapFileName.isEmpty()) { - skymapFileName = obj.get("skymapFileName").stringValue(""); + public void fromJson(JsonObject sky) { + rotation = sky.get("skyYaw").doubleValue(0); + mirrored = sky.get("skyMirrored").boolValue(true); + skyLightModifier = sky.get("skyLight").doubleValue(DEFAULT_INTENSITY); + mode = SkyMode.get(sky.get("mode").stringValue("")); + horizonOffset = sky.get("horizonOffset").doubleValue(0.0); + cloudsEnabled = sky.get("cloudsEnabled").boolValue(false); + cloudSize = sky.get("cloudSize").doubleValue(DEFAULT_CLOUD_SIZE); + cloudOffset.fromJson(sky.get("cloudOffset").object()); + + List theGradient = gradientFromJson(sky.get("gradient").array()); + if (theGradient != null && theGradient.size() >= 2) { + gradient = theGradient; + } + + switch (mode) { + case SKYMAP_PANORAMIC: + { + skymapFileName = sky.get("skymap").stringValue(""); + if (skymapFileName.isEmpty()) { + skymapFileName = sky.get("skymapFileName").stringValue(""); + } + break; + } + case SKYBOX: + { + JsonArray array = sky.get("skybox").array(); + for (int i = 0; i < 6; ++i) { + JsonValue value = array.get(i); + skyboxFileName[i] = value.stringValue(""); + } + break; + } + default: + break; + } + } + + /** + * Set the sky light modifier + * @param newValue + */ + public void setSkyLight(double newValue) { + skyLightModifier = newValue; + scene.refresh(); + } + + /** + * @return Current sky light modifier + */ + public double getSkyLight() { + return skyLightModifier; + } + + public void setGradient(List newGradient) { + gradient = new ArrayList(newGradient.size()); + for (Vector4d stop: newGradient) { + gradient.add(new Vector4d(stop)); + } + scene.refresh(); + } + + public List getGradient() { + List copy = new ArrayList(gradient.size()); + for (Vector4d stop: gradient) { + copy.add(new Vector4d(stop)); + } + return copy; + } + + public static JsonArray gradientJson(Collection gradient) { + JsonArray array = new JsonArray(); + for (Vector4d stop: gradient) { + JsonObject obj = new JsonObject(); + obj.add("rgb", Color.toString(stop.x, stop.y, stop.z)); + obj.add("pos", stop.w); + array.add(obj); + } + return array; + } + + /** + * @param array + * @return {@code null} if the gradient was not valid + */ + public static List gradientFromJson(JsonArray array) { + List gradient = new ArrayList(array.getNumElement()); + for (int i = 0; i < array.getNumElement(); ++i) { + JsonObject obj = array.getElement(i).object(); + Vector3d color = new Vector3d(); + try { + Color.fromString(obj.get("rgb").stringValue(""), 16, color); + Vector4d stop = new Vector4d(color.x, color.y, color.z, obj.get("pos").doubleValue(Double.NaN)); + if (!Double.isNaN(stop.w)) { + gradient.add(stop); + } + } catch (NumberFormatException e) { + } + } + boolean errors = false; + for (int i = 0; i < gradient.size(); ++i) { + Vector4d stop = gradient.get(i); + if (i == 0) { + if (stop.w != 0) { + errors = true; + break; + } + } else if (i < gradient.size()-1) { + if (stop.w < gradient.get(i-1).w) { + errors = true; + break; + } + } else { + if (stop.w != 1) { + errors = true; + break; + } + } + } + if (errors) { + // error in gradient data + return null; + } else { + return gradient; + } + } + + public static void makeDefaultGradient(Collection gradient) { + gradient.add(new Vector4d(9/255., 183/255., 217/255., 0)); + gradient.add(new Vector4d(212/255., 245/255., 251/255., 1)); + } + + public void loadSkyboxTexture(String fileName, int index) { + if (index < 0 || index >= 6) { + throw new IllegalArgumentException(); + } + skyboxFileName[index] = fileName; + skybox[index] = loadSkyTexture(fileName, skybox[index]); + scene.refresh(); + } + + private Texture loadSkyTexture(String fileName, Texture prevTexture) { + File textureFile = new File(fileName); + if (!textureFile.exists()) { + return prevTexture; + } + if (textureFile.exists()) { + try { + logger.info("Loading sky map: " + fileName); + if (fileName.toLowerCase().endsWith(".pfm")) { + return new PFMTexture(textureFile); + } else if (fileName.toLowerCase().endsWith(".hdr")) { + return new HDRTexture(textureFile); + } else { + return new SkymapTexture(ImageIO.read(textureFile)); + } + } catch (IOException e) { + logger.warn("Could not load skymap: " + fileName); + } catch (Throwable e) { + logger.error("Unexpected exception ocurred!", e); + } + } else { + logger.warn("Skymap could not be opened: " + fileName); + } + return prevTexture; + } + + public void setHorizonOffset(double newValue) { + newValue = Math.min(1, Math.max(0, newValue)); + if (newValue != horizonOffset) { + horizonOffset = newValue; + scene.refresh(); + } + } + + public double getHorizonOffset() { + return horizonOffset; + } + + + public void setCloudSize(double newValue) { + if (newValue != cloudSize) { + cloudSize = newValue; + if (cloudsEnabled) { + scene.refresh(); + } + } + } + + public double cloudSize() { + return cloudSize; + } + + public void setCloudXOffset(double newValue) { + if (newValue != cloudOffset.x) { + cloudOffset.x = newValue; + if (cloudsEnabled) { + scene.refresh(); + } + } + } + + /** + * Change the cloud height + * @param value + */ + public void setCloudYOffset(double newValue) { + if (newValue != cloudOffset.y) { + cloudOffset.y = newValue; + if (cloudsEnabled) { + scene.refresh(); + } + } + } + public void setCloudZOffset(double newValue) { + if (newValue != cloudOffset.z) { + cloudOffset.z = newValue; + if (cloudsEnabled) { + scene.refresh(); + } + } + } + + public double cloudXOffset() { + return cloudOffset.x; + } + + /** + * @return The current cloud height + */ + public double cloudYOffset() { + return cloudOffset.y; + } + + public double cloudZOffset() { + return cloudOffset.z; + } + + + /** + * Enable/disable clouds rendering + * @param newValue + */ + public void setCloudsEnabled(boolean newValue) { + if (newValue != cloudsEnabled) { + cloudsEnabled = newValue; + scene.refresh(); } - rotation = obj.get("skyYaw").doubleValue(0); - mirrored = obj.get("skyMirrored").boolValue(true); + } - JsonObject groundColorObj = obj.get("groundColor").object(); - groundColor.x = groundColorObj.get("red").doubleValue(1); - groundColor.y = groundColorObj.get("green").doubleValue(1); - groundColor.z = groundColorObj.get("blue").doubleValue(1); + /** + * @return true if cloud rendering is enabled + */ + public boolean cloudsEnabled() { + return cloudsEnabled; } + } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java index 1208ae6a58..85127e27a8 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Sun.java @@ -1,4 +1,4 @@ -/* Copyright (c) 2012-2013 Jesper Öqvist +/* Copyright (c) 2012-2014 Jesper Öqvist * * This file is part of Chunky. * @@ -16,7 +16,6 @@ */ package se.llbit.chunky.renderer.scene; -import java.awt.Color; import java.util.Random; import org.apache.commons.math3.util.FastMath; @@ -27,7 +26,6 @@ import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3d; -import se.llbit.math.Vector4d; import se.llbit.util.JSONifiable; import se.llbit.util.VectorPool; @@ -164,30 +162,37 @@ public class Sun implements JSONifiable { * Calculate skylight for ray * @param ray */ - public void skylight(Ray ray) { - Vector4d c = ray.color; - - if (ray.d.y < 0) { - ray.d.y = -ray.d.y; - } + public void calcSkyLight(Ray ray, double horizonOffset) { double cosTheta = ray.d.y; + cosTheta += horizonOffset * (1 - cosTheta); + if (cosTheta < 0) cosTheta = 0; double cosGamma = ray.d.dot(sw); double gamma = FastMath.acos(cosGamma); double cos2Gamma = cosGamma * cosGamma; - c.x = zenith_x * perezF(cosTheta, gamma, cos2Gamma, A.x, B.x, C.x, D.x, E.x) * f0_x; - c.y = zenith_y * perezF(cosTheta, gamma, cos2Gamma, A.y, B.y, C.y, D.y, E.y) * f0_y; - c.z = zenith_Y * perezF(cosTheta, gamma, cos2Gamma, A.z, B.z, C.z, D.z, E.z) * f0_Y; - c.z = 1 - FastMath.exp(-(1/17.) * c.z); - //c.z /= 20; - if (c.y <= Ray.EPSILON) { - c.set(0, 0, 0, 1); + double x = zenith_x * perezF(cosTheta, gamma, cos2Gamma, A.x, B.x, C.x, D.x, E.x) * f0_x; + double y = zenith_y * perezF(cosTheta, gamma, cos2Gamma, A.y, B.y, C.y, D.y, E.y) * f0_y; + double z = zenith_Y * perezF(cosTheta, gamma, cos2Gamma, A.z, B.z, C.z, D.z, E.z) * f0_Y; + if (y <= Ray.EPSILON) { + ray.color.set(0, 0, 0, 1); } else { - double f = (c.z / c.y); - c.set(c.x * f, c.z, (1 - c.x - c.y) * f, 1); + double f = (z / y); + double x2 = x * f; + double y2 = z; + double z2 = (1-x-y) * f; + // Old CIE-to-RGB matrix + /*ray.color.set( + 3.2410*x2 - 1.5374*y2 - 0.4986*z2, + -0.9692*x2 + 1.8760*y2 + 0.0416*z2, + 0.0556*x2 - 0.2040*y2 + 1.0570*z2, + 1);*/ + // new CIE to RGB M^-1 matrix from http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html + ray.color.set( + 2.3706743*x2 - 0.9000405*y2 - 0.4706338*z2, + -0.513885*x2 + 1.4253036*y2 + 0.0885814*z2, + 0.0052982*x2 - 0.0146949*y2 + 1.0093968*z2, + 1); + ray.color.scale(0.045); } - c.set(3.2410*c.x - 1.5374*c.y - 0.4986*c.z, - c.y = -0.9692*c.x + 1.8760*c.y + 0.0416*c.z, - 0.0556*c.x - 0.2040*c.y + 1.0570*c.z, 1); } private double chroma(double turb, double turb2, double sunTheta, @@ -340,12 +345,10 @@ public void flatShading(Ray ray) { } /** - * @param color + * @param newColor */ - public void setColor(java.awt.Color color) { - this.color.x = FastMath.pow(color.getRed() / 255., Scene.DEFAULT_GAMMA); - this.color.y = FastMath.pow(color.getGreen() / 255., Scene.DEFAULT_GAMMA); - this.color.z = FastMath.pow(color.getBlue() / 255., Scene.DEFAULT_GAMMA); + public void setColor(Vector3d newColor) { + this.color.set(newColor); initSun(); scene.refresh(); } @@ -472,16 +475,6 @@ public double inscatter(double Fex, double theta) { return ((Brt + Bmt) / (Br + Bm)) * (1 - Fex); } - /** - * @return An AWT Color object representing the current sun color - */ - public Color getAwtColor() { - return new Color( - (float) QuickMath.min(1, color.x), - (float) QuickMath.min(1, color.y), - (float) QuickMath.min(1, color.z)); - } - @Override public JsonObject toJson() { JsonObject sun = new JsonObject(); @@ -507,4 +500,11 @@ public void fromJson(JsonObject obj) { color.y = colorObj.get("green").doubleValue(1); color.z = colorObj.get("blue").doubleValue(1); } + + /** + * @return sun color + */ + public Vector3d getColor() { + return new Vector3d(color); + } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/ColorListener.java b/chunky/src/java/se/llbit/chunky/renderer/ui/ColorListener.java new file mode 100644 index 0000000000..0477c31c52 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/ColorListener.java @@ -0,0 +1,23 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import se.llbit.math.Vector3d; + +public interface ColorListener { + void onColorPicked(Vector3d color); +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/ColorPicker.java b/chunky/src/java/se/llbit/chunky/renderer/ui/ColorPicker.java new file mode 100644 index 0000000000..5fbafccdc5 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/ColorPicker.java @@ -0,0 +1,454 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Random; + +import javax.swing.AbstractAction; +import javax.swing.BorderFactory; +import javax.swing.GroupLayout; +import javax.swing.GroupLayout.Group; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.LayoutStyle.ComponentPlacement; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import se.llbit.math.Color; +import se.llbit.math.Constants; +import se.llbit.math.Vector3d; + +@SuppressWarnings("serial") +public class ColorPicker extends JDialog { + + private static final int NUM_SWATCHES = 10; + + private static final Vector3d[] staticHistory = new Vector3d[NUM_SWATCHES]; + static { + Random r = new Random(System.currentTimeMillis()); + for (int i = 0; i < NUM_SWATCHES; ++i) { + staticHistory[i] = new Vector3d(); + getStochasticColor(r, staticHistory[i], + (i+0.5)/10.0, 1, 0.5); + } + } + + private final JPanel currentSwatch = new JPanel(); + private final JPanel[] swatches = new JPanel[NUM_SWATCHES]; + private final JPanel[] history = new JPanel[NUM_SWATCHES]; + private final Vector3d[] swatchColors = new Vector3d[NUM_SWATCHES]; + private final Vector3d[] historyColors = new Vector3d[NUM_SWATCHES]; + private final JTextField colorHex = new JTextField(6); + private double hue = 0; + private double saturation = 1; + private double lightness = 0.5; + private final HuePicker huePicker; + private final LightnessPicker lightnessPicker; + private final SaturationPicker saturationPicker; + private final Vector3d currentColor = new Vector3d(); + private final Random r = new Random(System.currentTimeMillis()); + private final DocumentListener hexColorListener = new DocumentListener() { + @Override + public void removeUpdate(DocumentEvent e) { + update(); + } + @Override + public void insertUpdate(DocumentEvent e) { + update(); + } + @Override + public void changedUpdate(DocumentEvent e) { + update(); + } + private void update() { + String text = colorHex.getText(); + try { + int rgb = Integer.parseInt(text, 16); + Vector3d color = new Vector3d(); + Color.getRGBAComponents(rgb, color); + setColorNoHexUpdate(color); + } catch (NumberFormatException e) { + } + } + }; + + private final Collection listeners = new ArrayList(); + + public ColorPicker(JComponent parent, Vector3d color) { + huePicker = new HuePicker(this); + lightnessPicker = new LightnessPicker(this); + saturationPicker = new SaturationPicker(this); + + for (int i = 0; i < NUM_SWATCHES; ++i) { + swatchColors[i] = new Vector3d(); + swatches[i] = new JPanel(); + swatches[i].setPreferredSize(new Dimension(25, 25)); + final int index = i; + swatches[i].addMouseListener(new MouseListener() { + @Override + public void mouseClicked(MouseEvent e) { + } + @Override + public void mousePressed(MouseEvent e) { + setColor(swatchColors[index]); + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + }); + } + + for (int i = 0; i < NUM_SWATCHES; ++i) { + historyColors[i] = new Vector3d(); + history[i] = new JPanel(); + history[i].setPreferredSize(new Dimension(25, 25)); + final int index = i; + history[i].addMouseListener(new MouseListener() { + @Override + public void mouseClicked(MouseEvent e) { + } + @Override + public void mousePressed(MouseEvent e) { + setColor(historyColors[index]); + onColorEditFinished(); + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + }); + } + + setColor(color); + onColorEditFinished(); + initHistory(); + onColorChanged(); + + currentSwatch.setPreferredSize(new Dimension(300, 300)); + currentSwatch.addMouseListener(new MouseListener() { + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mousePressed(MouseEvent e) { + onColorPicked(); + closeDialog(); + } + @Override + public void mouseExited(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseClicked(MouseEvent e) { + } + }); + + JButton doneBtn = new JButton("Done"); + doneBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + onColorPicked(); + closeDialog(); + } + }); + JButton cancelBtn = new JButton("Cancel"); + cancelBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + closeDialog(); + } + }); + + addWindowFocusListener(new WindowFocusListener() { + @Override + public void windowLostFocus(WindowEvent e) { + closeDialog(); + } + @Override + public void windowGainedFocus(WindowEvent e) { + } + }); + + colorHex.getDocument().addDocumentListener(hexColorListener); + + setUndecorated(true); + setAlwaysOnTop(true); + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createTitledBorder("Color Picker")); + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + Group swatchesParallel = layout.createParallelGroup(); + for (JPanel swatch: swatches) { + swatchesParallel.addComponent(swatch); + } + Group swatchesSequential = layout.createSequentialGroup(); + boolean first = true; + for (JPanel swatch: swatches) { + if (!first) swatchesSequential.addGap(5); + first = false; + swatchesSequential.addComponent(swatch); + } + Group historyParallel = layout.createParallelGroup(); + for (JPanel swatch: history) { + historyParallel.addComponent(swatch); + } + Group historySequential = layout.createSequentialGroup(); + first = true; + for (JPanel swatch: history) { + if (!first) historySequential.addGap(5); + first = false; + historySequential.addComponent(swatch); + } + layout.setHorizontalGroup(layout.createParallelGroup() + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup() + .addComponent(huePicker) + .addComponent(lightnessPicker) + .addComponent(saturationPicker) + .addGroup(swatchesSequential) + .addGroup(historySequential) + ) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(currentSwatch) + ) + .addGroup(layout.createSequentialGroup() + .addComponent(colorHex, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(ComponentPlacement.UNRELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(cancelBtn) + .addPreferredGap(ComponentPlacement.UNRELATED) + .addComponent(doneBtn) + ) + ); + layout.setVerticalGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup() + .addGroup(layout.createSequentialGroup() + .addComponent(huePicker) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(lightnessPicker) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(saturationPicker) + .addPreferredGap(ComponentPlacement.RELATED) + .addGroup(swatchesParallel) + .addPreferredGap(ComponentPlacement.RELATED) + .addGroup(historyParallel) + ) + .addComponent(currentSwatch) + ) + .addPreferredGap(ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup() + .addComponent(colorHex) + .addComponent(cancelBtn) + .addComponent(doneBtn) + ) + ); + setContentPane(panel); + pack(); + + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Close Dialog"); + getRootPane().getActionMap().put("Close Dialog", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + closeDialog(); + } + }); + + getRootPane().setDefaultButton(doneBtn); + + Dimension parentSize = parent.getSize(); + Dimension size = getPreferredSize(); + Point loc = SwingUtilities.convertPoint(parent, + getLocation(), this); + int x = loc.x; + int y = loc.y+parentSize.height; + setBounds(x, y, size.width, size.height); + setVisible(true); + } + + protected void onColorPicked() { + updateHistory(); + Vector3d color = new Vector3d(); + se.llbit.math.Color.RGBfromHSL(color, hue, saturation, lightness); + for (ColorListener listener: listeners) { + listener.onColorPicked(color); + } + } + + protected void closeDialog() { + setVisible(false); + dispose(); + } + + protected void setHue(double value) { + this.hue = value; + saturationPicker.setHueLight(hue, lightness); + lightnessPicker.setHueSat(hue, saturation); + Color.RGBfromHSL(currentColor, hue, saturation, lightness); + updateHexColor(); + onColorChanged(); + } + + protected void setSaturation(double value) { + this.saturation = value; + lightnessPicker.setHueSat(hue, saturation); + Color.RGBfromHSL(currentColor, hue, saturation, lightness); + updateHexColor(); + onColorChanged(); + } + + protected void setLightness(double value) { + this.lightness = value; + saturationPicker.setHueLight(hue, lightness); + Color.RGBfromHSL(currentColor, hue, saturation, lightness); + updateHexColor(); + onColorChanged(); + } + + private void updateHexColor() { + colorHex.getDocument().removeDocumentListener(hexColorListener); + colorHex.setText(Color.toString(currentColor)); + colorHex.getDocument().addDocumentListener(hexColorListener); + } + + private void onColorChanged() { + currentSwatch.setBackground(new java.awt.Color((float)currentColor.x, (float)currentColor.y, (float)currentColor.z)); + currentSwatch.repaint(); + } + + private void setColor(Vector3d color) { + colorHex.setText(Color.toString(color)); + setColorNoHexUpdate(color); + } + private void setColorNoHexUpdate(Vector3d color) { + currentColor.set(color); + double r = color.x; + double g = color.y; + double b = color.z; + double alpha = 0.5 * (2*r - g - b); + double beta = Math.sqrt(3) * 0.5 * (g - b); + double angle = Math.atan2(beta, alpha); + if (angle < 0) { + angle = Constants.TAU + angle; + } + hue = angle / Constants.TAU; + double c = Math.sqrt(alpha*alpha + beta*beta); + lightness = 0.5*Math.max(r, Math.max(g, b)) + 0.5*Math.min(r, Math.min(g, b)); + double den = 1 - Math.abs(2*lightness - 1); + if (den > 0) { + saturation = Math.max(0, Math.min(1, c / den)); + } else { + saturation = 0; + } + saturationPicker.setHueLight(hue, lightness); + lightnessPicker.setHueSat(hue, saturation); + lightnessPicker.setLightness(lightness); + saturationPicker.setSaturation(saturation); + huePicker.setHue(hue); + onColorChanged(); + } + + private void initHistory() { + synchronized (ColorPicker.class) { + for (int i = 0; i < NUM_SWATCHES; ++i) { + historyColors[i].set(staticHistory[i]); + } + } + for (int i = 0; i < NUM_SWATCHES; ++i) { + history[i].setBackground(Color.toAWT(historyColors[i])); + history[i].repaint(); + } + } + + private void updateHistory() { + int rgb = Color.getRGB(currentColor); + int end = NUM_SWATCHES-1; + for (int i = 0; i < NUM_SWATCHES; ++i) { + if (Color.getRGB(historyColors[i]) == rgb) { + end = i; + break; + } + } + for (int i = end; i >= 1; --i) { + historyColors[i].set(historyColors[i-1]); + } + historyColors[0].set(currentColor); + synchronized (ColorPicker.class) { + for (int i = 0; i < NUM_SWATCHES; ++i) { + staticHistory[i].set(historyColors[i]); + } + } + } + + protected void onColorEditFinished() { + for (int i = 0; i < NUM_SWATCHES; ++i) { + getStochasticColor(r, swatchColors[i], hue, saturation, lightness); + swatches[i].setBackground(Color.toAWT(swatchColors[i])); + swatches[i].repaint(); + } + } + + private static void getStochasticColor(Random r, Vector3d color, + double hue, double saturation, double lightness) { + double x = r.nextDouble()*.25; + double y = r.nextDouble()*.75; + double z = r.nextDouble()*.25; + int sx = r.nextInt(3)-1; + int sy = r.nextInt(3)-1; + int sz = r.nextInt(3)-1; + double h = hue + sx*x*x; + double s = Math.max(0, Math.min(1, saturation + sy*y*y)); + double l = Math.max(0, Math.min(1, lightness + sz*z*z)); + if (h > 1) h -= 1; + else if (h < 0) h += 1; + se.llbit.math.Color.RGBfromHSL(color, h, s, l); + } + + public void addColorListener(ColorListener listener) { + listeners.add(listener); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/GradientEditor.java b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientEditor.java new file mode 100644 index 0000000000..dffa2850a7 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientEditor.java @@ -0,0 +1,272 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.GroupLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.LayoutStyle.ComponentPlacement; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import se.llbit.chunky.renderer.scene.Sky; +import se.llbit.chunky.ui.TextInputDialog; +import se.llbit.chunky.ui.TextInputListener; +import se.llbit.chunky.ui.TextOutputDialog; +import se.llbit.chunky.world.Icon; +import se.llbit.json.JsonParser; +import se.llbit.json.JsonParser.SyntaxError; +import se.llbit.math.Color; +import se.llbit.math.Vector3d; +import se.llbit.math.Vector4d; + +/** + * An editor for color gradients + * @author Jesper Öqvist + */ +@SuppressWarnings("serial") +public class GradientEditor extends JPanel implements GradientListener, TextInputListener { + + private final GradientUI gradientUI; + private final JButton prev = new JButton("<"); + private final JButton next = new JButton(">"); + private final JButton add = new JButton("+"); + private final JButton del = new JButton("-"); + private final JButton importBtn = new JButton(); + private final JButton exportBtn = new JButton(); + private final JTextField colorEdit = new JTextField(8); + private final JButton colorBtn = new JButton(); + private final JTextField posEdit = new JTextField(8); + private final DocumentListener documentListener = new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent e) { + update(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + update(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + update(); + } + + private void update() { + String text = colorEdit.getText(); + try { + Vector3d color = new Vector3d(); + Color.fromString(text, 16, color); + setColor(color); + } catch (NumberFormatException e) { + } + } + }; + + public GradientEditor() { + GroupLayout layout = new GroupLayout(this); + setLayout(layout); + + + Collection gradient = new LinkedList(); + Sky.makeDefaultGradient(gradient); + gradientUI = new GradientUI(gradient); + gradientUI.addGradientListener(this); + stopSelected(gradientUI.getSelctedIndex()); + + colorBtn.setIcon(Icon.colors.imageIcon()); + colorBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ColorPicker picker = new ColorPicker(colorBtn, getCurrentColor()); + picker.addColorListener(new ColorListener() { + @Override + public void onColorPicked(Vector3d color) { + setColor(color); + } + }); + } + }); + + next.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gradientUI.setSelectedIndex(gradientUI.getSelctedIndex()+1); + } + }); + + prev.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gradientUI.setSelectedIndex(gradientUI.getSelctedIndex()-1); + } + }); + + add.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gradientUI.addStop(); + } + }); + + del.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gradientUI.removeStop(); + } + }); + + colorEdit.getDocument().addDocumentListener(documentListener); + + importBtn.setIcon(Icon.save.imageIcon()); + importBtn.setToolTipText("Import a gradient"); + importBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JDialog dialog = new TextInputDialog( + "Import Gradient", "Gradient data:", + GradientEditor.this); + dialog.setVisible(true); + } + }); + exportBtn.setIcon(Icon.load.imageIcon()); + exportBtn.setToolTipText("Export current gradient"); + exportBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JDialog dialog = new TextOutputDialog( + "Export Gradient", "Gradient data:", + Sky.gradientJson(gradientUI.getGradient()).toCompactString()); + dialog.setVisible(true); + } + }); + + posEdit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JTextField source = (JTextField) e.getSource(); + String text = source.getText(); + double position = Double.parseDouble(text); + gradientUI.setPosition(position); + } + }); + + layout.setHorizontalGroup(layout.createParallelGroup() + .addGroup(layout.createSequentialGroup() + .addComponent(posEdit) + .addComponent(colorEdit) + .addComponent(colorBtn) + ) + .addGroup(layout.createSequentialGroup() + .addComponent(prev) + .addComponent(next) + .addComponent(del) + .addComponent(add) + .addPreferredGap(ComponentPlacement.UNRELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(importBtn) + .addComponent(exportBtn) + ) + .addComponent(gradientUI) + ); + layout.setVerticalGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup() + .addComponent(posEdit) + .addComponent(colorEdit) + .addComponent(colorBtn) + ) + .addComponent(gradientUI) + .addGroup(layout.createParallelGroup() + .addComponent(prev) + .addComponent(next) + .addComponent(del) + .addComponent(add) + .addComponent(importBtn) + .addComponent(exportBtn) + ) + ); + } + + protected void setColor(Vector3d newColor) { + gradientUI.setColor(newColor); + } + + protected Vector3d getCurrentColor() { + int index = gradientUI.getSelctedIndex(); + Vector4d stop = gradientUI.getStop(index); + return new Vector3d(stop.x, stop.y, stop.z); + } + + public void addGradientListener(GradientListener listener) { + gradientUI.addGradientListener(listener); + } + + @Override + public void gradientChanged(List newGradient) { + del.setEnabled(newGradient.size()>2); + } + + @Override + public void stopSelected(int index) { + Vector4d stop = gradientUI.getStop(index); + updateColorHex(stop.x, stop.y, stop.z); + posEdit.setText(""+stop.w); + prev.setEnabled(index > 0); + next.setEnabled(index < gradientUI.getNumStop()-1); + } + + @Override + public void stopModified(int index, Vector4d stop) { + colorEdit.setText(Color.toString(stop.x, stop.y, stop.z)); + posEdit.setText(""+stop.w); + } + + private void updateColorHex(double x, double y, double z) { + colorEdit.getDocument().removeDocumentListener(documentListener); + colorEdit.setText(Color.toString(x, y, z)); + colorEdit.getDocument().addDocumentListener(documentListener); + } + + @Override + public void onTextInput(String data) { + JsonParser parser = new JsonParser(new ByteArrayInputStream(data.getBytes())); + try { + List newGradient = Sky.gradientFromJson(parser.parse().array()); + if (newGradient != null) { + gradientUI.setGradient(newGradient); + } + } catch (IOException e) { + } catch (SyntaxError e) { + } + } + + public void setGradient(List newGradient) { + gradientUI.setGradient(newGradient); + } + +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/GradientListener.java b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientListener.java new file mode 100644 index 0000000000..857ab68def --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientListener.java @@ -0,0 +1,27 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import java.util.List; + +import se.llbit.math.Vector4d; + +public interface GradientListener { + void gradientChanged(List newGradient); + void stopSelected(int index); + void stopModified(int index, Vector4d marker); +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/GradientPicker.java b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientPicker.java new file mode 100644 index 0000000000..f5846b8a39 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientPicker.java @@ -0,0 +1,144 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JPanel; + +import se.llbit.math.Constants; +import se.llbit.math.Vector4d; + +@SuppressWarnings("serial") +abstract public class GradientPicker extends JPanel { + + private static final int MARKER_HEIGHT = 20; + private static final int MARKER_WIDTH = 7; + + protected final List gradient = new ArrayList(); + private BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + private int width = 1; + private int height = 1; + private double markerPos = 0; + + protected GradientPicker(final ColorPicker colorPicker) { + setPreferredSize(new Dimension(300, 40)); + addComponentListener(new ComponentListener() { + @Override + public void componentShown(ComponentEvent e) { + } + @Override + public void componentResized(ComponentEvent e) { + updateGradient(); + } + @Override + public void componentMoved(ComponentEvent e) { + } + @Override + public void componentHidden(ComponentEvent e) { + } + }); + MouseAdapter mouseAdapter = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + setMarkerAt(Math.max(0, Math.min(1, e.getX()/(double)width))); + onMarkerMoved(); + } + @Override + public void mouseReleased(MouseEvent e) { + colorPicker.onColorEditFinished(); + } + @Override + public void mouseDragged(MouseEvent e) { + setMarkerAt(Math.max(0, Math.min(1, e.getX()/(double)width))); + onMarkerMoved(); + } + }; + addMouseListener(mouseAdapter); + addMouseMotionListener(mouseAdapter); + } + + abstract protected void onMarkerMoved(); + + protected void setMarkerAt(double position) { + markerPos = position; + repaint(); + } + + protected double getPickerValue() { + return markerPos; + } + + protected void updateGradient() { + int newWidth = getWidth(); + int newHeight = getHeight(); + if (newWidth != 0 && newHeight != 0 && isVisible()) { + width = newWidth; + height = newHeight; + image = GradientUI.gradientImage(gradient, width, height); + repaint(); + } + } + + protected java.awt.Color getMarkerColor(double pos) { + int x = 0; + Vector4d c0 = gradient.get(x); + Vector4d c1 = gradient.get(x+1); + double xx = (pos - c0.w) / (c1.w-c0.w); + while (x+2 < gradient.size() && xx > 1) { + x += 1; + c0 = gradient.get(x); + c1 = gradient.get(x+1); + xx = (pos - c0.w) / (c1.w-c0.w); + } + xx = 0.5*(Math.sin(Math.PI*xx-Constants.HALF_PI)+1); + double a = 1-xx; + double b = xx; + return new java.awt.Color((float) (a*c0.x+b*c1.x), (float) (a*c0.y+b*c1.y), (float) (a*c0.z+b*c1.z), 1.f); + } + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + //Rectangle bounds = g.getClipBounds(); + g.drawImage(image, 0, 0, null); + + // fill color + g.setColor(java.awt.Color.WHITE); + int x = Math.min(width-1, (int) (markerPos * width)); + int[] xPoints = { x-MARKER_WIDTH, x, x+MARKER_WIDTH }; + int[] yPoints = { 0, MARKER_HEIGHT, 0 }; + g.fillPolygon(xPoints, yPoints, 3); + + g.setColor(java.awt.Color.BLACK); + g.drawLine(x-MARKER_WIDTH, 0, x, MARKER_HEIGHT); + g.drawLine(x, MARKER_HEIGHT, x+MARKER_WIDTH, 0); + g.drawLine(x-MARKER_WIDTH, 0, x+MARKER_WIDTH, 0); + } + +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/GradientUI.java b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientUI.java new file mode 100644 index 0000000000..2dda356744 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/GradientUI.java @@ -0,0 +1,405 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import java.awt.BasicStroke; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.swing.JPanel; + +import se.llbit.math.Constants; +import se.llbit.math.Vector3d; +import se.llbit.math.Vector4d; + +@SuppressWarnings("serial") +public class GradientUI extends JPanel { + + private final List gradient = new ArrayList(); + private BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + private int width = 1; + private int height = 1; + private int selected = 0; + + private final Collection listeners = new ArrayList(); + + public GradientUI(Collection gradient) { + if (gradient.size() < 2) { + throw new IllegalArgumentException("Too few gradient stops!"); + } + this.gradient.addAll(gradient); + + setPreferredSize(new Dimension(400, 60)); + addComponentListener(new ComponentListener() { + @Override + public void componentShown(ComponentEvent e) { + } + @Override + public void componentResized(ComponentEvent e) { + updateGradient(); + } + @Override + public void componentMoved(ComponentEvent e) { + } + @Override + public void componentHidden(ComponentEvent e) { + } + }); + MouseAdapter mouseAdapter = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + double x = e.getX() / (double) width; + closestStop(x); + } + + @Override + public void mouseDragged(MouseEvent e) { + moveStop(e.getX() / (double)width); + } + }; + addMouseListener(mouseAdapter); + addMouseMotionListener(mouseAdapter); + } + + /** + * Helper method to create a gradient image. + * @param gradient + * @param width + * @param height + * @return gradiant image + */ + public static final BufferedImage gradientImage(List gradient, int width, int height) { + if (width <= 0 || height <= 0 || gradient.size() < 2) { + throw new IllegalArgumentException(); + } + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + int x = 0; + for (int i = 0; i < width; ++i) { + double weight = i/(double)width; + Vector4d c0 = gradient.get(x); + Vector4d c1 = gradient.get(x+1); + double xx = (weight - c0.w) / (c1.w-c0.w); + while (x+2 < gradient.size() && xx > 1) { + x += 1; + c0 = gradient.get(x); + c1 = gradient.get(x+1); + xx = (weight - c0.w) / (c1.w-c0.w); + } + xx = 0.5*(Math.sin(Math.PI*xx-Constants.HALF_PI)+1); + double a = 1-xx; + double b = xx; + int argb = se.llbit.math.Color.getRGBA(a*c0.x+b*c1.x, a*c0.y+b*c1.y, a*c0.z+b*c1.z, 1); + for (int j = 0; j < height; ++j) { + image.setRGB(i, j, argb); + } + } + return image; + } + + public static final BufferedImage gradientImageLinear(List gradient, int width, int height) { + if (width <= 0 || height <= 0 || gradient.size() < 2) { + throw new IllegalArgumentException(); + } + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + int x = 0; + for (int i = 0; i < width; ++i) { + double weight = i/(double)width; + Vector4d c0 = gradient.get(x); + Vector4d c1 = gradient.get(x+1); + double xx = (weight - c0.w) / (c1.w-c0.w); + while (x+2 < gradient.size() && xx > 1) { + x += 1; + c0 = gradient.get(x); + c1 = gradient.get(x+1); + xx = (weight - c0.w) / (c1.w-c0.w); + } + double a = 1-xx; + double b = xx; + int argb = se.llbit.math.Color.getRGBA(a*c0.x+b*c1.x, a*c0.y+b*c1.y, a*c0.z+b*c1.z, 1); + for (int j = 0; j < height; ++j) { + image.setRGB(i, j, argb); + } + } + return image; + } + + public void updateGradient() { + int newWidth = getWidth(); + int newHeight = getHeight(); + if (newWidth > 0 && newHeight > 0) { + width = newWidth; + height = newHeight; + image = gradientImage(gradient, width, height); + int x = 0; + if (gradient.size() < 2) { + return; + } + for (int i = 0; i < newWidth; ++i) { + double weight = i/(double)newWidth; + Vector4d c0 = gradient.get(x); + Vector4d c1 = gradient.get(x+1); + double xx = (weight - c0.w) / (c1.w-c0.w); + while (x+2 < gradient.size() && xx > 1) { + x += 1; + c0 = gradient.get(x); + c1 = gradient.get(x+1); + xx = (weight - c0.w) / (c1.w-c0.w); + } + xx = 0.5*(Math.sin(Math.PI*xx-Constants.HALF_PI)+1); + double a = 1-xx; + double b = xx; + int argb = se.llbit.math.Color.getRGBA(a*c0.x+b*c1.x, a*c0.y+b*c1.y, a*c0.z+b*c1.z, 1); + for (int j = 0; j < newHeight; ++j) { + image.setRGB(i, j, argb); + } + } + } + } + + private static Stroke thickLine = new BasicStroke(2); + private static java.awt.Color lightGray = new java.awt.Color(220, 220, 220); + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + //Rectangle bounds = g.getClipBounds(); + g.drawImage(image, 0, 0, null); + g.setColor(java.awt.Color.WHITE); + g.fillRect(0, 0, width, 15); + int min = 0, max = width; + if (selected > 0) { + min = (int) (gradient.get(selected-1).w * width); + } + if (selected < gradient.size()-1) { + max = (int) (gradient.get(selected+1).w * width); + } + g.setColor(lightGray); + g.fillRect(min, 0, max-min, 15); + int index = 0; + for (Vector4d stop: gradient) { + if (index == selected) { + index += 1; + continue; + } + + // fill color + g.setColor(java.awt.Color.WHITE); + int x = Math.min(width-1, (int) (stop.w*width)); + int[] xPoints = { x-5, x, x+5 }; + int[] yPoints = { 0, 15, 0 }; + g.fillPolygon(xPoints, yPoints, 3); + + // border color + boolean isEndpoint = index == 0 || index == gradient.size()-1; + if (isEndpoint) { + g.setColor(java.awt.Color.GRAY); + } else { + g.setColor(java.awt.Color.BLACK); + } + g.drawLine(x-5, 0, x, 15); + g.drawLine(x, 15, x+5, 0); + g.drawLine(x-5, 0, x+5, 0); + index += 1; + } + g.setStroke(thickLine); + Vector4d stop = gradient.get(selected); + // fill color + boolean isEndpoint = selected == 0 || selected == gradient.size()-1; + if (isEndpoint) { + g.setColor(java.awt.Color.GRAY); + } else { + g.setColor(java.awt.Color.BLACK); + } + int x = Math.min(width-1, (int) (stop.w*width)); + int[] xPoints = { x-5, x, x+5 }; + int[] yPoints = { 0, 15, 0 }; + g.fillPolygon(xPoints, yPoints, 3); + + // border color + g.setColor(java.awt.Color.WHITE); + g.drawLine(x-5, 0, x, 15); + g.drawLine(x, 15, x+5, 0); + g.drawLine(x-5, 0, x+5, 0); + index += 1; + } + + protected void moveStop(double d) { + if (selected > 0 && selected < gradient.size()-1) { + double min = gradient.get(selected-1).w; + double max = gradient.get(selected+1).w; + gradient.get(selected).w = Math.max(min, Math.min(max, d)); + gradientChanged(); + repaint(); + fireStopModifiedNotification(); + } + } + + private void fireStopModifiedNotification() { + for (GradientListener listener: listeners) { + listener.stopModified(selected, gradient.get(selected)); + } + } + + private void gradientChanged() { + updateGradient(); + repaint(); + fireGradientChangedNotification(); + } + + private void fireGradientChangedNotification() { + for (GradientListener listener: listeners) { + listener.gradientChanged(gradient); + } + + } + + protected int closestStop(double x) { + double closest = Double.MAX_VALUE; + int stop = 0; + int index = 0; + for (Vector4d m: gradient) { + double distance = Math.abs(m.w - x); + if (distance < closest) { + stop = index; + closest = distance; + } + index += 1; + } + setSelectedIndex(stop); + return stop; + } + + public void addGradientListener(GradientListener listener) { + this.listeners.add(listener); + } + + /** + * Set selected gradient stop. + * @param index + */ + public void setSelectedIndex(int index) { + if (index >= 0 && index < gradient.size()) { + selected = index; + repaint(); + fireStopSelectedNotification(); + } + } + + private void fireStopSelectedNotification() { + for (GradientListener listener: listeners) { + listener.stopSelected(selected); + } + } + + /** + * @return currently selected stop index + */ + public int getSelctedIndex() { + return selected; + } + + /** + * @return number of gradient stops + */ + public int getNumStop() { + return gradient.size(); + } + + /** + * @param index + * @return stop with index 'index' + */ + public Vector4d getStop(int index) { + return gradient.get(index); + } + + public void setColor(Vector3d newColor) { + Vector4d stop = gradient.get(selected); + stop.x = newColor.x; + stop.y = newColor.y; + stop.z = newColor.z; + gradientChanged(); + } + + public void setPosition(double newPosition) { + Vector4d stop = gradient.get(selected); + stop.w = newPosition; + gradientChanged(); + } + + public void addStop() { + int i0; + int i1; + if (selected == gradient.size()-1) { + i0 = selected-1; + i1 = selected; + } else { + i0 = selected; + i1 = selected+1; + } + gradient.add(i1, blend(gradient.get(i0), gradient.get(i1), 0.5)); + selected = i1; + fireStopSelectedNotification(); + gradientChanged(); + } + + private Vector4d blend(Vector4d s0, Vector4d s1, double d) { + double xx = 0.5*(Math.sin(Math.PI*d-Constants.HALF_PI)+1); + double a = 1-xx; + double b = xx; + return new Vector4d(a*s0.x+b*s1.x, a*s0.y+b*s1.y, a*s0.z+b*s1.z, a*s0.w+b*s1.w); + } + + public void removeStop() { + if (gradient.size() > 2) { + gradient.remove(selected); + if (selected == 0) { + gradient.get(0).w = 0; + } else if (selected == gradient.size()) { + gradient.get(selected-1).w = 1; + } + if (selected > 0) { + selected -= 1; + } + fireStopSelectedNotification(); + gradientChanged(); + } + } + + public void setGradient(List newGradient) { + gradient.clear(); + gradient.addAll(newGradient); + gradientChanged(); + } + + public Collection getGradient() { + return gradient; + } + +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/HuePicker.java b/chunky/src/java/se/llbit/chunky/renderer/ui/HuePicker.java new file mode 100644 index 0000000000..ab90471405 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/HuePicker.java @@ -0,0 +1,48 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import se.llbit.math.Vector4d; + +@SuppressWarnings("serial") +public class HuePicker extends GradientPicker { + + private final ColorPicker colorPicker; + + public HuePicker(final ColorPicker colorPicker) { + super(colorPicker); + this.colorPicker = colorPicker; + + gradient.add(new Vector4d(1, 0, 0, 0.00)); + gradient.add(new Vector4d(1, 1, 0, 1/6.0)); + gradient.add(new Vector4d(0, 1, 0, 2/6.0)); + gradient.add(new Vector4d(0, 1, 1, 3/6.0)); + gradient.add(new Vector4d(0, 0, 1, 4/6.0)); + gradient.add(new Vector4d(1, 0, 1, 5/6.0)); + gradient.add(new Vector4d(1, 0, 0, 1.00)); + } + + public void setHue(double hue) { + setMarkerAt(hue); + } + + @Override + protected void onMarkerMoved() { + colorPicker.setHue(getPickerValue()); + } + +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/LightnessPicker.java b/chunky/src/java/se/llbit/chunky/renderer/ui/LightnessPicker.java new file mode 100644 index 0000000000..b909adb377 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/LightnessPicker.java @@ -0,0 +1,52 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import se.llbit.math.Vector3d; +import se.llbit.math.Vector4d; + +@SuppressWarnings("serial") +public class LightnessPicker extends GradientPicker { + + private final ColorPicker colorPicker; + + public LightnessPicker(final ColorPicker colorPicker) { + super(colorPicker); + this.colorPicker = colorPicker; + + gradient.add(new Vector4d(0, 0, 0, 0.0)); + gradient.add(new Vector4d(0.5, 0.5, 0.5, 0.5)); + gradient.add(new Vector4d(1, 1, 1, 1.0)); + } + + @Override + protected void onMarkerMoved() { + colorPicker.setLightness(getPickerValue()); + } + + protected void setLightness(double value) { + setMarkerAt(value); + } + + private final Vector3d tmp = new Vector3d(); + protected void setHueSat(double hue, double sat) { + se.llbit.math.Color.RGBfromHSL(tmp, hue, sat, .5); + gradient.get(1).set(tmp.x, tmp.y, tmp.z, 0.5); + updateGradient(); + } + +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/RenderControls.java b/chunky/src/java/se/llbit/chunky/renderer/ui/RenderControls.java index c9eeb87d56..41d4567d39 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/ui/RenderControls.java +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/RenderControls.java @@ -33,17 +33,18 @@ import java.text.ParseException; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.swing.BorderFactory; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.GroupLayout; import javax.swing.GroupLayout.Alignment; import javax.swing.JButton; import javax.swing.JCheckBox; -import javax.swing.JColorChooser; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; @@ -92,6 +93,7 @@ import se.llbit.json.JsonObject; import se.llbit.math.QuickMath; import se.llbit.math.Vector3d; +import se.llbit.math.Vector4d; import se.llbit.ui.Adjuster; /** @@ -118,8 +120,13 @@ public class RenderControls extends JDialog implements ViewListener, private final NumberFormat numberFormat = NumberFormat.getInstance(); - private final JSlider skyRotationSlider = new JSlider(); + private final JSlider skymapRotationSlider = new JSlider(); + private final JSlider skyboxRotationSlider = new JSlider(); private final JButton loadSkymapBtn = new JButton(); + private final JPanel simulatedSkyPanel = new JPanel(); + private final JPanel skymapPanel = new JPanel(); + private final JPanel skyGradientPanel = new JPanel(); + private final JPanel skyboxPanel = new JPanel(); private final JCheckBox mirrorSkyCB = new JCheckBox(); private final JComboBox canvasSizeCB = new JComboBox(); private final JComboBox cameraPreset = new JComboBox(); @@ -152,7 +159,6 @@ public class RenderControls extends JDialog implements ViewListener, private final JComboBox postprocessCB = new JComboBox(); private final JComboBox skyModeCB = new JComboBox(); private final JButton changeSunColorBtn = new JButton(); - private final JButton changeGroundColorBtn = new JButton(); private final JLabel etaLbl = new JLabel(); private final JCheckBox waterWorldCB = new JCheckBox(); private final JTextField waterHeightField = new JTextField(); @@ -181,11 +187,29 @@ public class RenderControls extends JDialog implements ViewListener, private JPanel skyPane; private JPanel advancedPane; + private final Adjuster skyHorizonOffset = new Adjuster( + "Horizon offset", + "Moves the horizon below the actual horizon", + 0.0, 1.0) { + @Override + public void valueChanged(double newValue) { + renderMan.scene().sky().setHorizonOffset(newValue); + } + + @Override + public void update() { + set(renderMan.scene().sky().getHorizonOffset()); + } + }; + private final Adjuster numThreads = new Adjuster( "Render threads", "Number of rendering threads", RenderConstants.NUM_RENDER_THREADS_MIN, 20) { + { + setClampMax(false); + } @Override public void valueChanged(double newValue) { int value = (int) newValue; @@ -230,39 +254,50 @@ public void update() { set(renderMan.scene().getRayDepth()); } }; - private final Adjuster cloudHeight = new Adjuster( - "Cloud Height", - "Height of the cloud layer", -128, 512) { + private final Adjuster emitterIntensity = new Adjuster( + "Emitter intensity", + "Light intensity modifier for emitters", + Scene.MIN_EMITTER_INTENSITY, + Scene.MAX_EMITTER_INTENSITY) { + { + setLogarithmicMode(); + } @Override public void valueChanged(double newValue) { - renderMan.scene().setCloudHeight((int) newValue); + renderMan.scene().setEmitterIntensity(newValue); } @Override public void update() { - set(renderMan.scene().getCloudHeight()); + set(renderMan.scene().getEmitterIntensity()); } }; - private final Adjuster emitterIntensity = new Adjuster( - "Emitter intensity", - "Light intensity modifier for emitters", - Scene.MIN_EMITTER_INTENSITY, - Scene.MAX_EMITTER_INTENSITY) { + private final Adjuster skyLight = new Adjuster( + "Sky Light", + "Sky light intensity modifier", + Sky.MIN_INTENSITY, + Sky.MAX_INTENSITY) { + { + setLogarithmicMode(); + } @Override public void valueChanged(double newValue) { - renderMan.scene().setEmitterIntensity(newValue); + renderMan.scene().sky().setSkyLight(newValue); } @Override public void update() { - set(renderMan.scene().getEmitterIntensity()); + set(renderMan.scene().sky().getSkyLight()); } }; private final Adjuster sunIntensity = new Adjuster( - "Sun intensity", - "Light intensity modifier for sun", + "Sun Intensity", + "Sunlight intensity modifier", Sun.MIN_INTENSITY, Sun.MAX_INTENSITY) { + { + setLogarithmicMode(); + } @Override public void valueChanged(double newValue) { renderMan.scene().sun().setIntensity(newValue); @@ -306,6 +341,9 @@ public void update() { "Field of View", 1.0, 180.0) { + { + setClampMax(false); + } @Override public void valueChanged(double newValue) { renderMan.scene().camera().setFoV(newValue); @@ -323,6 +361,9 @@ public void update() { "Distance to focal plane", Camera.MIN_SUBJECT_DISTANCE, Camera.MAX_SUBJECT_DISTANCE) { + { + setLogarithmicMode(); + } @Override public void valueChanged(double newValue) { renderMan.scene().camera().setSubjectDistance(newValue); @@ -338,6 +379,9 @@ public void update() { "exposure", Scene.MIN_EXPOSURE, Scene.MAX_EXPOSURE) { + { + setLogarithmicMode(); + } @Override public void valueChanged(double newValue) { renderMan.scene().setExposure(newValue); @@ -348,9 +392,70 @@ public void update() { set(renderMan.scene().getExposure()); } }; + private final Adjuster cloudSize = new Adjuster( + "Cloud Size", + "Cloud Size", + 1.0, 128.0) { + { + setLogarithmicMode(); + } + @Override + public void valueChanged(double newValue) { + renderMan.scene().sky().setCloudSize(newValue); + } + + @Override + public void update() { + set(renderMan.scene().sky().cloudSize()); + } + }; + private final Adjuster cloudXOffset = new Adjuster( + "Cloud X", + "Cloud X Offset", + 1.0, 100.0) { + @Override + public void valueChanged(double newValue) { + renderMan.scene().sky().setCloudXOffset(newValue); + } + + @Override + public void update() { + set(renderMan.scene().sky().cloudXOffset()); + } + }; + private final Adjuster cloudYOffset = new Adjuster( + "Cloud Y", + "Height of the cloud layer", + -128.0, 512.0) { + @Override + public void valueChanged(double newValue) { + renderMan.scene().sky().setCloudYOffset(newValue); + } + + @Override + public void update() { + set(renderMan.scene().sky().cloudYOffset()); + } + }; + private final Adjuster cloudZOffset = new Adjuster( + "Cloud Z", + "Cloud Z Offset", + 1.0, 100.0) { + @Override + public void valueChanged(double newValue) { + renderMan.scene().sky().setCloudZOffset(newValue); + } + + @Override + public void update() { + set(renderMan.scene().sky().cloudZOffset()); + } + }; Set safeComponents = new HashSet(); + private GradientEditor gradientEditor; + { // initialize safe component set safeComponents.add(saveSceneBtn); @@ -448,9 +553,9 @@ public void windowActivated(WindowEvent e) { updateTitle(); addTab("General", Icon.wrench, generalPane = buildGeneralPane()); - addTab("Lighting", null, lightingPane = buildLightingPane()); - addTab("Sky", null, skyPane = buildSkyPane()); - addTab("Camera", null, cameraPane = buildCameraPane()); + addTab("Lighting", Icon.colors, lightingPane = buildLightingPane()); + addTab("Sky", Icon.sky, skyPane = buildSkyPane()); + addTab("Camera", Icon.camera, cameraPane = buildCameraPane()); addTab("Post-processing", null, buildPostProcessingPane()); addTab("Advanced", null, advancedPane = buildAdvancedPane()); addTab("Help", Texture.unknown, buildHelpPane()); @@ -487,7 +592,7 @@ public void actionPerformed(ActionEvent e) { }); startRenderBtn.setText("START"); - startRenderBtn.setIcon(Icon.play.createIcon()); + startRenderBtn.setIcon(Icon.play.imageIcon()); startRenderBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -514,7 +619,7 @@ public void actionPerformed(ActionEvent e) { } }); - lockBtn.setIcon(Icon.lock.createIcon()); + lockBtn.setIcon(Icon.lock.imageIcon()); lockBtn.setToolTipText("Lock or unlock the render controls"); lockBtn.addActionListener(new ActionListener() { @Override @@ -528,7 +633,7 @@ public void actionPerformed(ActionEvent e) { }); stopRenderBtn.setText("RESET"); - stopRenderBtn.setIcon(Icon.stop.createIcon()); + stopRenderBtn.setIcon(Icon.stop.imageIcon()); stopRenderBtn.setToolTipText("Warning: this will discard the " + "current rendered image!
Make sure to save your image " + "before stopping the renderer!"); @@ -563,7 +668,7 @@ public void actionPerformed(ActionEvent e) { updateSceneNameField(); saveSceneBtn.setText("Save"); - saveSceneBtn.setIcon(Icon.disk.createIcon()); + saveSceneBtn.setIcon(Icon.disk.imageIcon()); saveSceneBtn.addActionListener(saveSceneListener); JPanel panel = new JPanel(); @@ -677,7 +782,7 @@ private void addTab(String title, Texture icon, Component component) { tabbedPane.add(title, component); if (icon != null) { - JLabel lbl = new JLabel(title, icon.createIcon(), SwingConstants.RIGHT); + JLabel lbl = new JLabel(title, icon.imageIcon(), SwingConstants.RIGHT); lbl.setIconTextGap(5); tabbedPane.setTabComponentAt(index, lbl); } @@ -690,7 +795,6 @@ private JPanel buildAdvancedPane() { JSeparator sep2 = new JSeparator(); JSeparator sep3 = new JSeparator(); - numThreads.setClampMax(false); numThreads.update(); cpuLoad.update(); @@ -809,7 +913,6 @@ public boolean accept(File dir, String name) { } private JPanel buildPostProcessingPane() { - exposure.setLogarithmicMode(); exposure.update(); JLabel postprocessDescLbl = new JLabel("Post processing affects rendering performance
when the preview window is visible"); @@ -873,6 +976,7 @@ private JPanel buildGeneralPane() { updateCanvasSizeField(); loadSceneBtn.setText("Load Scene"); + loadSceneBtn.setIcon(Icon.load.imageIcon()); loadSceneBtn.addActionListener(loadSceneListener); JButton loadSelectedChunksBtn = new JButton("Load Selected Chunks"); @@ -885,7 +989,7 @@ public void actionPerformed(ActionEvent e) { }); JButton reloadChunksBtn = new JButton("Reload Chunks"); - reloadChunksBtn.setIcon(Icon.reload.createIcon()); + reloadChunksBtn.setIcon(Icon.reload.imageIcon()); reloadChunksBtn.setToolTipText("Reload all chunks in the scene"); reloadChunksBtn.addActionListener(new ActionListener() { @Override @@ -1069,15 +1173,17 @@ public void actionPerformed(ActionEvent arg0) { private JPanel buildLightingPane() { changeSunColorBtn.setText("Change Sun Color"); + changeSunColorBtn.setIcon(Icon.colors.imageIcon()); changeSunColorBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - java.awt.Color newColor = JColorChooser.showDialog( - RenderControls.this, "Choose Sun Color", - renderMan.scene().sun().getAwtColor()); - if (newColor != null) { - renderMan.scene().sun().setColor(newColor); - } + ColorPicker picker = new ColorPicker(changeSunColorBtn, renderMan.scene().sun().getColor()); + picker.addColorListener(new ColorListener() { + @Override + public void onColorPicked(Vector3d color) { + renderMan.scene().sun().setColor(color); + } + }); } }); @@ -1089,12 +1195,12 @@ public void actionPerformed(ActionEvent e) { enableEmitters.setSelected(renderMan.scene().getEmittersEnabled()); enableEmitters.addActionListener(emittersListener); - emitterIntensity.setLogarithmicMode(); emitterIntensity.update(); - sunIntensity.setLogarithmicMode(); sunIntensity.update(); + skyLight.update(); + sunAzimuth.update(); sunAltitude.update(); @@ -1109,18 +1215,21 @@ public void actionPerformed(ActionEvent e) { .addComponent(enableEmitters) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup() + .addComponent(skyLight.getLabel()) .addComponent(emitterIntensity.getLabel()) .addComponent(sunIntensity.getLabel()) .addComponent(sunAzimuth.getLabel()) .addComponent(sunAltitude.getLabel()) ) .addGroup(layout.createParallelGroup() + .addComponent(skyLight.getSlider()) .addComponent(emitterIntensity.getSlider()) .addComponent(sunIntensity.getSlider()) .addComponent(sunAzimuth.getSlider()) .addComponent(sunAltitude.getSlider()) ) .addGroup(layout.createParallelGroup() + .addComponent(skyLight.getField()) .addComponent(emitterIntensity.getField()) .addComponent(sunIntensity.getField()) .addComponent(sunAzimuth.getField()) @@ -1133,6 +1242,8 @@ public void actionPerformed(ActionEvent e) { ); layout.setVerticalGroup(layout.createSequentialGroup() .addContainerGap() + .addGroup(skyLight.verticalGroup(layout)) + .addPreferredGap(ComponentPlacement.UNRELATED) .addComponent(enableEmitters) .addPreferredGap(ComponentPlacement.RELATED) .addGroup(emitterIntensity.verticalGroup(layout)) @@ -1156,50 +1267,163 @@ private JPanel buildSkyPane() { JLabel skyModeLbl = new JLabel("Sky Mode:"); skyModeCB.setModel(new DefaultComboBoxModel(Sky.SkyMode.values())); skyModeCB.addActionListener(skyModeListener); - // TODO implement sky modes - skyModeLbl.setVisible(false); - skyModeCB.setVisible(false); updateSkyMode(); - JLabel skyRotationLbl = new JLabel("Skymap rotation:"); - skyRotationSlider.setMinimum(1); - skyRotationSlider.setMaximum(100); - skyRotationSlider.addChangeListener(skyRotationListener); - skyRotationSlider.setToolTipText("Controls the horizontal rotational offset for the skymap"); + JLabel skymapRotationLbl = new JLabel("Skymap rotation:"); + skymapRotationSlider.setMinimum(1); + skymapRotationSlider.setMaximum(100); + skymapRotationSlider.addChangeListener(skyRotationListener); + skymapRotationSlider.setToolTipText("Controls the horizontal rotational offset for the skymap"); + JLabel skyboxRotationLbl = new JLabel("Skybox rotation:"); + skyboxRotationSlider.setMinimum(1); + skyboxRotationSlider.setMaximum(100); + skyboxRotationSlider.addChangeListener(skyRotationListener); + skyboxRotationSlider.setToolTipText("Controls the horizontal rotational offset for the skymap"); updateSkyRotation(); + skyHorizonOffset.update(); + cloudSize.update(); + cloudXOffset.update(); + cloudYOffset.update(); + cloudZOffset.update(); + + simulatedSkyPanel.setBorder(BorderFactory.createTitledBorder("Simulated Sky Settings")); + GroupLayout simulatedSkyLayout = new GroupLayout(simulatedSkyPanel); + simulatedSkyPanel.setLayout(simulatedSkyLayout); + simulatedSkyLayout.setAutoCreateContainerGaps(true); + simulatedSkyLayout.setAutoCreateGaps(true); + simulatedSkyLayout.setHorizontalGroup(simulatedSkyLayout.createParallelGroup() + .addGroup(skyHorizonOffset.horizontalGroup(simulatedSkyLayout)) + ); + simulatedSkyLayout.setVerticalGroup(simulatedSkyLayout.createSequentialGroup() + .addGroup(skyHorizonOffset.verticalGroup(simulatedSkyLayout)) + ); + + skymapPanel.setBorder(BorderFactory.createTitledBorder("Skymap Settings")); + GroupLayout skymapLayout = new GroupLayout(skymapPanel); + skymapPanel.setLayout(skymapLayout); + skymapLayout.setAutoCreateContainerGaps(true); + skymapLayout.setAutoCreateGaps(true); + skymapLayout.setHorizontalGroup(skymapLayout.createParallelGroup() + .addComponent(loadSkymapBtn) + .addGroup(skymapLayout.createSequentialGroup() + .addComponent(skymapRotationLbl) + .addComponent(skymapRotationSlider) + ) + .addComponent(mirrorSkyCB) + ); + skymapLayout.setVerticalGroup(skymapLayout.createSequentialGroup() + .addComponent(loadSkymapBtn) + .addGroup(skymapLayout.createParallelGroup() + .addComponent(skymapRotationLbl) + .addComponent(skymapRotationSlider) + ) + .addComponent(mirrorSkyCB) + ); + loadSkymapBtn.setText("Load Skymap"); loadSkymapBtn.setToolTipText("Use a panoramic skymap"); - loadSkymapBtn.addActionListener(loadSkymapListener); - - JSeparator sep1 = new JSeparator(); + loadSkymapBtn.addActionListener(new SkymapTextureLoader(renderMan)); mirrorSkyCB.setText("Mirror sky at horizon"); mirrorSkyCB.addActionListener(mirrorSkyListener); updateMirroSkyCB(); - changeGroundColorBtn.setText("Change Ground Color"); - changeGroundColorBtn.addActionListener(new ActionListener() { + skyGradientPanel.setBorder(BorderFactory.createTitledBorder("Sky Gradient")); + gradientEditor = new GradientEditor(); + updateSkyGradient(); + skyGradientPanel.add(gradientEditor); + gradientEditor.addGradientListener(new GradientListener() { @Override - public void actionPerformed(ActionEvent e) { - java.awt.Color newColor = JColorChooser.showDialog( - RenderControls.this, "Choose Ground Color", - renderMan.scene().sky().getGroundColor()); - if (newColor != null) { - renderMan.scene().sky().setGroundColor(newColor); - } + public void gradientChanged(List newGradient) { + renderMan.scene().sky().setGradient(newGradient); } - }); - - JButton unloadSkymapBtn = new JButton("Unload Skymap"); - unloadSkymapBtn.setToolTipText("Use the default sky"); - unloadSkymapBtn.addActionListener(new ActionListener() { @Override - public void actionPerformed(ActionEvent e) { - renderMan.scene().sky().unloadSkymap(); + public void stopSelected(int index) { + } + @Override + public void stopModified(int index, Vector4d marker) { } }); + GroupLayout skyboxLayout = new GroupLayout(skyboxPanel); + skyboxPanel.setLayout(skyboxLayout); + skyboxPanel.setBorder(BorderFactory.createTitledBorder("Skybox")); + + JLabel skyboxLbl = new JLabel("Load skybox textures:"); + + JButton loadUpTexture = new JButton("Up"); + loadUpTexture.setToolTipText("Load up texture"); + loadUpTexture.setIcon(Icon.skyboxUp.imageIcon()); + loadUpTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_UP)); + + JButton loadDownTexture = new JButton("Down"); + loadDownTexture.setToolTipText("Load down texture"); + loadDownTexture.setIcon(Icon.skyboxDown.imageIcon()); + loadDownTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_DOWN)); + + JButton loadFrontTexture = new JButton("Front"); + loadFrontTexture.setToolTipText("Load front (north) texture"); + loadFrontTexture.setIcon(Icon.skyboxFront.imageIcon()); + loadFrontTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_FRONT)); + + JButton loadBackTexture = new JButton("Back"); + loadBackTexture.setToolTipText("Load back (south) texture"); + loadBackTexture.setIcon(Icon.skyboxBack.imageIcon()); + loadBackTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_BACK)); + + JButton loadRightTexture = new JButton("Right"); + loadRightTexture.setToolTipText("Load right (east) texture"); + loadRightTexture.setIcon(Icon.skyboxRight.imageIcon()); + loadRightTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_RIGHT)); + + JButton loadLeftTexture = new JButton("Left"); + loadLeftTexture.setToolTipText("Load left (west) texture"); + loadLeftTexture.setIcon(Icon.skyboxLeft.imageIcon()); + loadLeftTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_LEFT)); + + skyboxLayout.setAutoCreateContainerGaps(true); + skyboxLayout.setAutoCreateGaps(true); + skyboxLayout.setHorizontalGroup(skyboxLayout.createParallelGroup() + .addGroup(skyboxLayout.createSequentialGroup() + .addComponent(skyboxLbl) + .addGroup(skyboxLayout.createParallelGroup() + .addComponent(loadUpTexture) + .addComponent(loadFrontTexture) + .addComponent(loadRightTexture) + ) + .addGroup(skyboxLayout.createParallelGroup() + .addComponent(loadDownTexture) + .addComponent(loadBackTexture) + .addComponent(loadLeftTexture) + ) + ) + .addGroup(skyboxLayout.createSequentialGroup() + .addComponent(skyboxRotationLbl) + .addComponent(skyboxRotationSlider) + ) + ); + skyboxLayout.setVerticalGroup(skyboxLayout.createSequentialGroup() + .addComponent(skyboxLbl) + .addGroup(skyboxLayout.createParallelGroup() + .addComponent(loadUpTexture) + .addComponent(loadDownTexture) + ) + .addGroup(skyboxLayout.createParallelGroup() + .addComponent(loadFrontTexture) + .addComponent(loadBackTexture) + ) + .addGroup(skyboxLayout.createParallelGroup() + .addComponent(loadRightTexture) + .addComponent(loadLeftTexture) + ) + .addGroup(skyboxLayout.createParallelGroup() + .addComponent(skyboxRotationLbl) + .addComponent(skyboxRotationSlider) + ) + ); + + atmosphereEnabled.setText("enable atmosphere"); atmosphereEnabled.addActionListener(atmosphereListener); updateAtmosphereCheckBox(); @@ -1212,8 +1436,6 @@ public void actionPerformed(ActionEvent e) { cloudsEnabled.addActionListener(cloudsEnabledListener); updateCloudsEnabledCheckBox(); - cloudHeight.update(); - JPanel panel = new JPanel(); GroupLayout layout = new GroupLayout(panel); panel.setLayout(layout); @@ -1225,22 +1447,17 @@ public void actionPerformed(ActionEvent e) { .addPreferredGap(ComponentPlacement.RELATED) .addComponent(skyModeCB, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) ) - .addGroup(layout.createSequentialGroup() - .addComponent(skyRotationLbl) - .addComponent(skyRotationSlider) - ) - .addGroup(layout.createSequentialGroup() - .addComponent(loadSkymapBtn) - .addPreferredGap(ComponentPlacement.RELATED) - .addComponent(unloadSkymapBtn) - ) - .addComponent(mirrorSkyCB) - .addComponent(changeGroundColorBtn) - .addComponent(sep1) + .addComponent(simulatedSkyPanel) + .addComponent(skymapPanel) + .addComponent(skyGradientPanel) + .addComponent(skyboxPanel) .addComponent(atmosphereEnabled) .addComponent(volumetricFogEnabled) .addComponent(cloudsEnabled) - .addGroup(cloudHeight.horizontalGroup(layout)) + .addGroup(cloudSize.horizontalGroup(layout)) + .addGroup(cloudXOffset.horizontalGroup(layout)) + .addGroup(cloudYOffset.horizontalGroup(layout)) + .addGroup(cloudZOffset.horizontalGroup(layout)) ) .addContainerGap() ); @@ -1251,21 +1468,10 @@ public void actionPerformed(ActionEvent e) { .addComponent(skyModeCB) ) .addPreferredGap(ComponentPlacement.UNRELATED) - .addGroup(layout.createParallelGroup() - .addComponent(loadSkymapBtn) - .addComponent(unloadSkymapBtn) - ) - .addPreferredGap(ComponentPlacement.UNRELATED) - .addGroup(layout.createParallelGroup() - .addComponent(skyRotationLbl) - .addComponent(skyRotationSlider) - ) - .addPreferredGap(ComponentPlacement.UNRELATED) - .addComponent(mirrorSkyCB) - .addPreferredGap(ComponentPlacement.UNRELATED) - .addComponent(changeGroundColorBtn) - .addPreferredGap(ComponentPlacement.UNRELATED) - .addComponent(sep1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(simulatedSkyPanel) + .addComponent(skymapPanel) + .addComponent(skyGradientPanel) + .addComponent(skyboxPanel) .addPreferredGap(ComponentPlacement.UNRELATED) .addComponent(atmosphereEnabled) .addPreferredGap(ComponentPlacement.UNRELATED) @@ -1273,7 +1479,13 @@ public void actionPerformed(ActionEvent e) { .addPreferredGap(ComponentPlacement.UNRELATED) .addComponent(cloudsEnabled) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(cloudHeight.verticalGroup(layout)) + .addGroup(cloudSize.verticalGroup(layout)) + .addPreferredGap(ComponentPlacement.RELATED) + .addGroup(cloudXOffset.verticalGroup(layout)) + .addPreferredGap(ComponentPlacement.RELATED) + .addGroup(cloudYOffset.verticalGroup(layout)) + .addPreferredGap(ComponentPlacement.RELATED) + .addGroup(cloudZOffset.verticalGroup(layout)) .addContainerGap() ); return panel; @@ -1282,13 +1494,11 @@ public void actionPerformed(ActionEvent e) { private JPanel buildCameraPane() { JLabel projectionModeLbl = new JLabel("Projection"); - fov.setClampMax(false); fov.update(); dof = new DoFAdjuster(renderMan); dof.update(); - subjectDistance.setLogarithmicMode(); subjectDistance.update(); JLabel presetLbl = new JLabel("Preset:"); @@ -1296,9 +1506,9 @@ private JPanel buildCameraPane() { CameraPreset.NONE, CameraPreset.ISO_WEST_NORTH, CameraPreset.ISO_NORTH_EAST, CameraPreset.ISO_EAST_SOUTH, CameraPreset.ISO_SOUTH_WEST, - CameraPreset.SKYBOX_EAST, CameraPreset.SKYBOX_WEST, + CameraPreset.SKYBOX_RIGHT, CameraPreset.SKYBOX_LEFT, CameraPreset.SKYBOX_UP, CameraPreset.SKYBOX_DOWN, - CameraPreset.SKYBOX_NORTH, CameraPreset.SKYBOX_SOUTH, + CameraPreset.SKYBOX_FRONT, CameraPreset.SKYBOX_BACK, }; cameraPreset.setModel(new DefaultComboBoxModel(presets)); cameraPreset.setMaximumRowCount(presets.length); @@ -1661,7 +1871,7 @@ protected void updateVolumetricFogCheckBox() { protected void updateCloudsEnabledCheckBox() { cloudsEnabled.removeActionListener(cloudsEnabledListener); - cloudsEnabled.setSelected(renderMan.scene().cloudsEnabled()); + cloudsEnabled.setSelected(renderMan.scene().sky().cloudsEnabled()); cloudsEnabled.addActionListener(cloudsEnabledListener); } @@ -1799,27 +2009,6 @@ public void actionPerformed(ActionEvent e) { new SceneSelector(RenderControls.this, context); } }; - private final ActionListener loadSkymapListener = new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - CenteredFileDialog fileDialog = - new CenteredFileDialog(null, "Open Skymap", FileDialog.LOAD); - fileDialog.setDirectory(System.getProperty("user.dir")); - fileDialog.setFilenameFilter( - new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(".png") - || name.toLowerCase().endsWith(".jpg"); - } - }); - fileDialog.setVisible(true); - File selectedFile = fileDialog.getSelectedFile(); - if (selectedFile != null) { - renderMan.scene().sky().loadSkyMap(selectedFile.getAbsolutePath()); - } - } - }; private final ChangeListener skyRotationListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { @@ -1862,6 +2051,8 @@ public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) { JComboBox source = (JComboBox) e.getSource(); renderMan.scene().sky().setSkyMode((SkyMode) source.getSelectedItem()); + updateSkyMode(); + RenderControls.this.pack(); } }; private final ActionListener cameraPositionListener = new ActionListener() { @@ -1945,7 +2136,7 @@ public void actionPerformed(ActionEvent e) { @Override public void actionPerformed(ActionEvent e) { JCheckBox source = (JCheckBox) e.getSource(); - renderMan.scene().setCloudsEnabled(source.isSelected()); + renderMan.scene().sky().setCloudsEnabled(source.isSelected()); } }; private final ActionListener mirrorSkyListener = new ActionListener() { @@ -1989,10 +2180,18 @@ protected void updateWaterHeight() { } protected void updateSkyRotation() { - skyRotationSlider.removeChangeListener(skyRotationListener); - skyRotationSlider.setValue((int) FastMath.round( + skymapRotationSlider.removeChangeListener(skyRotationListener); + skymapRotationSlider.setValue((int) FastMath.round( 100 * renderMan.scene().sky().getRotation() / (2 * Math.PI))); - skyRotationSlider.addChangeListener(skyRotationListener); + skymapRotationSlider.addChangeListener(skyRotationListener); + skyboxRotationSlider.removeChangeListener(skyRotationListener); + skyboxRotationSlider.setValue((int) FastMath.round( + 100 * renderMan.scene().sky().getRotation() / (2 * Math.PI))); + skyboxRotationSlider.addChangeListener(skyRotationListener); + } + + private void updateSkyGradient() { + gradientEditor.setGradient(renderMan.scene().sky().getGradient()); } protected void updateProjectionMode() { @@ -2004,7 +2203,12 @@ protected void updateProjectionMode() { protected void updateSkyMode() { skyModeCB.removeActionListener(skyModeListener); - skyModeCB.setSelectedItem(renderMan.scene().sky().getSkyMode()); + SkyMode mode = renderMan.scene().sky().getSkyMode(); + skyModeCB.setSelectedItem(mode); + simulatedSkyPanel.setVisible(mode == SkyMode.SIMULATED); + skymapPanel.setVisible(mode == SkyMode.SKYMAP_PANORAMIC || mode == SkyMode.SKYMAP_SPHERICAL); + skyGradientPanel.setVisible(mode == SkyMode.GRADIENT); + skyboxPanel.setVisible(mode == SkyMode.SKYBOX); skyModeCB.addActionListener(skyModeListener); } @@ -2255,13 +2459,16 @@ public void sceneLoaded() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { + skyHorizonOffset.update(); dof.update(); fov.update(); subjectDistance.update(); updateProjectionMode(); + updateSkyGradient(); updateSkyMode(); updateCanvasSizeField(); emitterIntensity.update(); + skyLight.update(); sunIntensity.update(); sunAzimuth.update(); sunAltitude.update(); @@ -2281,7 +2488,10 @@ public void run() { updateSPPTargetField(); updateSceneNameField(); updatePostprocessCB(); - cloudHeight.update(); + cloudSize.update(); + cloudXOffset.update(); + cloudYOffset.update(); + cloudZOffset.update(); rayDepth.update(); updateWaterHeight(); updateCameraDirection(); @@ -2410,16 +2620,16 @@ public void renderStateChanged(boolean pathTrace, boolean paused) { lock = true; if (paused) { startRenderBtn.setText("RESUME"); - startRenderBtn.setIcon(Icon.play.createIcon()); + startRenderBtn.setIcon(Icon.play.imageIcon()); } else { startRenderBtn.setText("PAUSE"); - startRenderBtn.setIcon(Icon.pause.createIcon()); + startRenderBtn.setIcon(Icon.pause.imageIcon()); } stopRenderBtn.setEnabled(true); stopRenderBtn.setForeground(Color.red); } else { startRenderBtn.setText("START"); - startRenderBtn.setIcon(Icon.play.createIcon()); + startRenderBtn.setIcon(Icon.play.imageIcon()); stopRenderBtn.setEnabled(false); stopRenderBtn.setForeground(Color.black); } @@ -2436,7 +2646,7 @@ private void lockControls() { lockPane(skyPane); lockPane(lightingPane); lockPane(advancedPane); - lockBtn.setIcon(Icon.key.createIcon()); + lockBtn.setIcon(Icon.key.imageIcon()); controlsLocked = true; } @@ -2446,7 +2656,7 @@ private void unlockControls() { unlockPane(skyPane); unlockPane(lightingPane); unlockPane(advancedPane); - lockBtn.setIcon(Icon.lock.createIcon()); + lockBtn.setIcon(Icon.lock.imageIcon()); controlsLocked = false; } @@ -2498,4 +2708,83 @@ protected int getDumpFrequency() { } } } + static class SkymapTextureLoader implements ActionListener { + private final RenderManager renderMan; + private static String defaultDirectory = System.getProperty("user.dir"); + public SkymapTextureLoader(RenderManager renderMan) { + this.renderMan = renderMan; + } + @Override + public void actionPerformed(ActionEvent e) { + CenteredFileDialog fileDialog = + new CenteredFileDialog(null, "Open Skymap", FileDialog.LOAD); + String directory; + synchronized (SkyboxTextureLoader.class) { + directory = defaultDirectory; + } + fileDialog.setDirectory(directory); + fileDialog.setFilenameFilter( + new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".png") + || name.toLowerCase().endsWith(".jpg") + || name.toLowerCase().endsWith(".hdr") + || name.toLowerCase().endsWith(".pfm"); + } + }); + fileDialog.setVisible(true); + File selectedFile = fileDialog.getSelectedFile(); + if (selectedFile != null) { + synchronized (SkyboxTextureLoader.class) { + File parent = selectedFile.getParentFile(); + if (parent != null) { + defaultDirectory = parent.getAbsolutePath(); + } + } + renderMan.scene().sky().loadSkymap(selectedFile.getAbsolutePath()); + } + } + }; + + static class SkyboxTextureLoader implements ActionListener { + private final RenderManager renderMan; + private final int textureIndex; + private static String defaultDirectory = System.getProperty("user.dir"); + public SkyboxTextureLoader(RenderManager renderMan, int textureIndex) { + this.renderMan = renderMan; + this.textureIndex = textureIndex; + } + @Override + public void actionPerformed(ActionEvent e) { + CenteredFileDialog fileDialog = + new CenteredFileDialog(null, "Open Skybox Texture", FileDialog.LOAD); + String directory; + synchronized (SkyboxTextureLoader.class) { + directory = defaultDirectory; + } + fileDialog.setDirectory(directory); + fileDialog.setFilenameFilter( + new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".png") + || name.toLowerCase().endsWith(".jpg") + || name.toLowerCase().endsWith(".hdr") + || name.toLowerCase().endsWith(".pfm"); + } + }); + fileDialog.setVisible(true); + File selectedFile = fileDialog.getSelectedFile(); + if (selectedFile != null) { + synchronized (SkyboxTextureLoader.class) { + File parent = selectedFile.getParentFile(); + if (parent != null) { + defaultDirectory = parent.getAbsolutePath(); + } + } + renderMan.scene().sky().loadSkyboxTexture(selectedFile.getAbsolutePath(), textureIndex); + } + } + }; } diff --git a/chunky/src/java/se/llbit/chunky/renderer/ui/SaturationPicker.java b/chunky/src/java/se/llbit/chunky/renderer/ui/SaturationPicker.java new file mode 100644 index 0000000000..51d6971221 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/ui/SaturationPicker.java @@ -0,0 +1,53 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.renderer.ui; + +import se.llbit.math.Vector3d; +import se.llbit.math.Vector4d; + +@SuppressWarnings("serial") +public class SaturationPicker extends GradientPicker { + + private final ColorPicker colorPicker; + + public SaturationPicker(final ColorPicker colorPicker) { + super(colorPicker); + this.colorPicker = colorPicker; + + gradient.add(new Vector4d(0, 0, 0, 0.00)); + gradient.add(new Vector4d(1, 1, 1, 1.00)); + } + + @Override + protected void onMarkerMoved() { + colorPicker.setSaturation(getPickerValue()); + } + + private final Vector3d tmp = new Vector3d(); + public void setHueLight(double hue, double lightness) { + se.llbit.math.Color.RGBfromHSL(tmp, hue, 0, lightness); + gradient.get(0).set(tmp.x, tmp.y, tmp.z, 0); + se.llbit.math.Color.RGBfromHSL(tmp, hue, 1, lightness); + gradient.get(1).set(tmp.x, tmp.y, tmp.z, 1); + updateGradient(); + } + + public void setSaturation(double value) { + setMarkerAt(value); + } + +} diff --git a/chunky/src/java/se/llbit/chunky/resources/AbstractHDRITexture.java b/chunky/src/java/se/llbit/chunky/resources/AbstractHDRITexture.java new file mode 100644 index 0000000000..604b21beed --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/resources/AbstractHDRITexture.java @@ -0,0 +1,84 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.resources; + +import se.llbit.math.Vector4d; + +public class AbstractHDRITexture extends Texture { + public float[] buf; + + @Override + public void getColorInterpolated(double u, double v, Vector4d sample) { + double x = width * u; + double y = height * v; + int x0 = clamp(floor(x), width); + int x1 = clamp(ceil(x), width); + int y0 = clamp(floor(y), height); + int y1 = clamp(ceil(y), height); + double xw = 1 - x + x0; + double yw = 1 - y + y0; + int offset = (y0*width + x0)*3; + double r0 = buf[offset+0]; + double g0 = buf[offset+1]; + double b0 = buf[offset+2]; + offset = (y0*width + x1)*3; + double r1 = buf[offset+0]; + double g1 = buf[offset+1]; + double b1 = buf[offset+2]; + offset = (y1*width + x0)*3; + double r2 = buf[offset+0]; + double g2 = buf[offset+1]; + double b2 = buf[offset+2]; + offset = (y1*width + x1)*3; + double r3 = buf[offset+0]; + double g3 = buf[offset+1]; + double b3 = buf[offset+2]; + sample.set( + r0*xw*yw + r1*(1-xw)*yw + r2*xw*(1-yw) + r3*(1-xw)*(1-yw), + g0*xw*yw + g1*(1-xw)*yw + g2*xw*(1-yw) + g3*(1-xw)*(1-yw), + b0*xw*yw + b1*(1-xw)*yw + b2*xw*(1-yw) + b3*(1-xw)*(1-yw), + 1 + ); + } + + @Override + public void getColor(double u, double v, Vector4d c) { + int x = (int) (width * u); + int y = (int) (height * v); + x = (x<0)?0:(x>=width)?width-1:x; + y = (y<0)?0:(y>=height)?height-1:y; + int offset = (y*width + x)*3; + c.set(buf[offset+0], buf[offset+1], buf[offset+2], 1); + } + + /** + * Clamp image coordinate. + */ + private static final int clamp(int i, int end) { + return i < 0 ? 0 : (i >= end ? end-1 : i); + } + + private static final int floor(double d) { + int i = (int) d; + return d < i ? i-1 : i; + } + + private static final int ceil(double d) { + int i = (int) d; + return d > i ? i+1 : i; + } +} diff --git a/chunky/src/java/se/llbit/chunky/resources/HDRTexture.java b/chunky/src/java/se/llbit/chunky/resources/HDRTexture.java new file mode 100644 index 0000000000..c06d50c9b9 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/resources/HDRTexture.java @@ -0,0 +1,179 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.resources; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HDRTexture extends AbstractHDRITexture { + public float[] buf; + + public HDRTexture(File file) { + // This RGBE loader was created to mimic the behavior of the RADIANCE + // rendering system (http://radsite.lbl.gov/). I studied the sources + // (src/common/color.c) to understand how RADIANCE worked, then wrote this + // code from scratch in an attempt to implement the same interface. + try { + RandomAccessFile raf = new RandomAccessFile(file, "r"); + String fmt = raf.readLine(); + if (!fmt.equals("#?RADIANCE")) { + throw new Error("not a recognized HDR format! Can only handle RGBE!"); + } + boolean haveFormat = false; + String format = ""; + while (true) { + + String cmd = raf.readLine(); + if (cmd.trim().isEmpty()) { + break; + } + if (cmd.startsWith("FORMAT=")) { + haveFormat = true; + format = cmd; + } + } + if (!haveFormat) { + throw new Error("could not find image format!"); + } + if (!format.equals("FORMAT=32-bit_rle_rgbe")) { + throw new Error("only 32-bit RGBE HDR format supported!"); + } + String resolution = raf.readLine(); + Pattern regex = Pattern.compile("-Y\\s(\\d+)\\s\\+X\\s(\\d+)"); + Matcher matcher = regex.matcher(resolution); + if (!matcher.matches()) { + throw new Error("unrecognized pixel order"); + } + width = Integer.parseInt(matcher.group(2)); + height = Integer.parseInt(matcher.group(1)); + + long start = raf.getFilePointer(); + long byteBufLen = raf.length() - start; + FileChannel channel = raf.getChannel(); + MappedByteBuffer byteBuf = channel.map(FileChannel.MapMode.READ_ONLY, start, byteBufLen); + + // precompute exponents + double exp[] = new double[256]; + for (int e = 0; e < 256; ++e) { + exp[e] = Math.pow(2, e-136); + } + + buf = new float[width*height*3]; + byte[][] scanbuf = new byte[width][4]; + for (int i = 0; i < height; ++i) { + readScanline(byteBuf, scanbuf, width); + + int offset = (height-i-1)*width*3; + for (int x = 0; x < width; ++x) { + int r = 0xFF&scanbuf[x][0]; + int g = 0xFF&scanbuf[x][1]; + int b = 0xFF&scanbuf[x][2]; + int e = 0xFF&scanbuf[x][3]; + if (e == 0) { + buf[offset+0] = 0; + buf[offset+1] = 0; + buf[offset+2] = 0; + } else { + double f = exp[e]; + buf[offset+0] = (float) ((r+0.5)*f); + buf[offset+1] = (float) ((g+0.5)*f); + buf[offset+2] = (float) ((b+0.5)*f); + } + offset += 3; + } + } + raf.close(); + } catch (IOException e) { + System.err.println("Error loading PFM: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void readScanline(MappedByteBuffer byteBuf, byte[][] scanline, int len) { + byte h0 = byteBuf.get(); + byte h1 = byteBuf.get(); + byte h2 = byteBuf.get(); + byte h3 = byteBuf.get(); + if (h0 != 2 || h1 != 2 || (h2&0x80) != 0) { + scanline[0][0] = h0; + scanline[0][1] = h1; + scanline[0][2] = h2; + scanline[0][3] = h2; + readScanlineOldFmt(byteBuf, scanline, len); + } + + int width = ((0xFF&h2)<<8) | (0xFF&h3); + if (width != len) { + throw new Error("length mismatch"); + } + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < width; ) { + int code = 0xFF & byteBuf.get(); + if (code > 128) { + int num = 0x7F & code; + if (j+num > width) { + throw new Error("scanline overrun"); + } + byte value = byteBuf.get(); + while (num-- > 0) { + scanline[j++][i] = value; + } + } else { + int num = code; + if (j+num > width) { + throw new Error("scanline overrun"); + } + while (num-- > 0) { + scanline[j++][i] = byteBuf.get(); + } + } + + } + } + + } + + private void readScanlineOldFmt(MappedByteBuffer byteBuf, byte[][] scanline, int len) { + int shift = 0; + for (int i = 1; i < len; ) { + scanline[i][0] = byteBuf.get(); + scanline[i][1] = byteBuf.get(); + scanline[i][2] = byteBuf.get(); + scanline[i][3] = byteBuf.get(); + if (scanline[i][0] == 1 && scanline[i][1] == 1 && scanline[i][2] == 1) { + int num = scanline[i][3]< + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.resources; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Scanner; + +public class PFMTexture extends AbstractHDRITexture { + + public PFMTexture(File file) { + try { + FileInputStream in = new FileInputStream(file); + Scanner scan = new Scanner(in); + String fmt = scan.next(); + int components = 3; + if (fmt.equals("PF")) { + components = 3; + } else if (fmt.equals("Pf")) { + components = 1; + } else { + System.err.println("Warning: unknown PFM format!"); + } + System.out.println(fmt); + width = Integer.parseInt(scan.next()); + System.out.println(width); + height = Integer.parseInt(scan.next()); + System.out.println(height); + float endianScale = Float.parseFloat(scan.next()); + boolean bigEndian = true; + float scale;// not used yet + if (endianScale < 0) { + scale = -endianScale; + bigEndian = false; + } else { + scale = endianScale; + } + System.out.println(endianScale); + scan.close(); + RandomAccessFile f = new RandomAccessFile(file, "r"); + long len = f.length(); + long start = len - width*height*components*4; + buf = new float[width*height*3]; + int offset = 0; + + FileChannel channel = f.getChannel(); + MappedByteBuffer byteBuf = channel.map(FileChannel.MapMode.READ_ONLY, start, buf.length*4); + if (bigEndian) { + byteBuf.order(ByteOrder.BIG_ENDIAN); + } else { + byteBuf.order(ByteOrder.LITTLE_ENDIAN); + } + while (offset < buf.length) { + if (components == 3) { + buf[offset+0] = byteBuf.getFloat(); + buf[offset+1] = byteBuf.getFloat(); + buf[offset+2] = byteBuf.getFloat(); + } else { + buf[offset+0] = + buf[offset+1] = + buf[offset+2] = byteBuf.getFloat(); + } + offset += 3; + } + f.close(); + } catch (IOException e) { + System.err.println("Error loading PFM: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/chunky/src/java/se/llbit/chunky/resources/Texture.java b/chunky/src/java/se/llbit/chunky/resources/Texture.java index 2039e93eab..b2f162f2b6 100644 --- a/chunky/src/java/se/llbit/chunky/resources/Texture.java +++ b/chunky/src/java/se/llbit/chunky/resources/Texture.java @@ -38,6 +38,21 @@ @SuppressWarnings("javadoc") public class Texture { + public static final Texture EMPTY_TEXTURE = new Texture() { + @Override + public void getColor(double u, double v, Vector4d c) { + c.set(0,0,0,0); + }; + @Override + public void getColorInterpolated(double u, double v, Vector4d c) { + c.set(0,0,0,0); + }; + @Override + public boolean isEmptyTexture() { + return true; + }; + }; + public static final Texture air = new Texture("air"); public static final Texture stone = new Texture("stone"); public static final Texture prismarine = new Texture(); @@ -586,30 +601,30 @@ public final float[] getColor(int x, int y) { */ public void getColorInterpolated(double u, double v, Vector4d c) { - double x = u * width; - double y = v * (height-1); + double x = u * (width-1); + double y = (1-v) * (height-1); double weight; int fx = (int) QuickMath.floor(x); int cx = (int) QuickMath.ceil(x); int fy = (int) QuickMath.floor(y); int cy = (int) QuickMath.ceil(y); - float[] rgb = getColor(fx % width, fy); + float[] rgb = getColor(fx, fy); weight = (1 - (y-fy)) * (1 - (x-fx)); c.x = weight * rgb[0]; c.y = weight * rgb[1]; c.z = weight * rgb[2]; - rgb = getColor(cx % width, fy); + rgb = getColor(cx, fy); weight = (1 - (y-fy)) * (1 - (cx-x)); c.x += weight * rgb[0]; c.y += weight * rgb[1]; c.z += weight * rgb[2]; - rgb = getColor(fx % width, cy); + rgb = getColor(fx, cy); weight = (1 - (cy-y)) * (1 - (x-fx)); c.x += weight * rgb[0]; c.y += weight * rgb[1]; c.z += weight * rgb[2]; - rgb = getColor(cx % width, cy); + rgb = getColor(cx, cy); weight = (1 - (cy-y)) * (1 - (cx-x)); c.x += weight * rgb[0]; c.y += weight * rgb[1]; @@ -648,11 +663,16 @@ public BufferedImage getImage() { return image; } + private ImageIcon imageIcon = null; + /** * @return An ImageIcon containing this texture's internal image */ - public ImageIcon createIcon() { - return new ImageIcon(image); + synchronized public ImageIcon imageIcon() { + if (imageIcon == null) { + imageIcon = new ImageIcon(image); + } + return imageIcon; } /** @@ -661,4 +681,11 @@ public ImageIcon createIcon() { public int getWidth() { return width; } + + /** + * @return {@code true} if this is the dedicated empty texture + */ + public boolean isEmptyTexture() { + return false; + } } diff --git a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java index b9b5b3bd47..0bae827b19 100644 --- a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java +++ b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java @@ -263,7 +263,7 @@ public void componentHidden(ComponentEvent e) { }); JMenuItem createScene = new JMenuItem("New 3D scene..."); - createScene.setIcon(Icon.chunky.createIcon()); + createScene.setIcon(Icon.sky.imageIcon()); createScene.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -271,6 +271,7 @@ public void actionPerformed(ActionEvent e) { } }); JMenuItem loadScene = new JMenuItem("Load scene..."); + loadScene.setIcon(Icon.load.imageIcon()); loadScene.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { diff --git a/chunky/src/java/se/llbit/chunky/ui/Controls.java b/chunky/src/java/se/llbit/chunky/ui/Controls.java index 8c01097607..895ccf5329 100644 --- a/chunky/src/java/se/llbit/chunky/ui/Controls.java +++ b/chunky/src/java/se/llbit/chunky/ui/Controls.java @@ -134,12 +134,12 @@ private void initComponents() { tabbedPane = new JTabbedPane(); - addTab("Map View", Icon.map.createIcon(), buildViewPanel(), "Change map view"); - addTab("Chunks", Icon.mapSelected.createIcon(), buildEditPanel(), "Chunk operations"); - addTab("Highlight", Icon.redTorchOn.createIcon(), buildHighlightPanel(), "Change block highlight settings"); - addTab("Options", Icon.wrench.createIcon(), buildOptionsPanel(), "Configure Chunky"); - addTab("3D Render", Icon.chunky.createIcon(), buildRenderPanel(), "Render chunks in 3D"); - addTab("About", Texture.unknown.createIcon(), buildAboutPanel(), "About Chunky"); + addTab("Map View", Icon.map.imageIcon(), buildViewPanel(), "Change map view"); + addTab("Chunks", Icon.mapSelected.imageIcon(), buildEditPanel(), "Chunk operations"); + addTab("Highlight", Icon.redTorchOn.imageIcon(), buildHighlightPanel(), "Change block highlight settings"); + addTab("Options", Icon.wrench.imageIcon(), buildOptionsPanel(), "Configure Chunky"); + addTab("3D Render", Icon.sky.imageIcon(), buildRenderPanel(), "Render chunks in 3D"); + addTab("About", Texture.unknown.imageIcon(), buildAboutPanel(), "About Chunky"); JButton selectWorldBtn = new JButton(); selectWorldBtn.setText(Messages.getString("Controls.SelectWorld_lbl")); //$NON-NLS-1$ @@ -151,7 +151,7 @@ public void actionPerformed(ActionEvent arg0) { }); JButton reloadWorldBtn = new JButton("Reload"); - reloadWorldBtn.setIcon(Icon.reload.createIcon()); + reloadWorldBtn.setIcon(Icon.reload.imageIcon()); reloadWorldBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -304,6 +304,7 @@ private JComponent buildOptionsPanel() { JComponent optionsPanel = new JPanel(); JButton loadTexturePackBtn = new JButton("Load Resource Pack"); + loadTexturePackBtn.setIcon(Icon.load.imageIcon()); loadTexturePackBtn.setToolTipText("Load a custom resource pack"); loadTexturePackBtn.addActionListener(new ActionListener() { @Override @@ -421,6 +422,7 @@ public void actionPerformed(ActionEvent e) { }); JButton loadSceneBtn = new JButton("Load Scene"); + loadSceneBtn.setIcon(Icon.load.imageIcon()); loadSceneBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { diff --git a/chunky/src/java/se/llbit/chunky/ui/TextInputDialog.java b/chunky/src/java/se/llbit/chunky/ui/TextInputDialog.java new file mode 100644 index 0000000000..0da776a6b5 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/TextInputDialog.java @@ -0,0 +1,123 @@ +/* Copyright (c) 2012 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.ui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.GroupLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.LayoutStyle.ComponentPlacement; + +/** + * Dedicated error reporting dialog. + * + * Used to display critical errors in a nicer way. + * + * @author Jesper Öqvist + */ +@SuppressWarnings("serial") +public class TextInputDialog extends JDialog { + + /** + * Initialize the error dialog. + */ + public TextInputDialog(String title, String message, final TextInputListener listener) { + super(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setModalityType(ModalityType.APPLICATION_MODAL); + + setTitle(title); + setLocationRelativeTo(null); + + JPanel panel = new JPanel(); + JLabel lbl = new JLabel(message); + final JTextField textField = new JTextField(40); + + JButton cancelBtn = new JButton("Cancel"); + cancelBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + closeDialog(); + } + }); + + JButton okBtn = new JButton("Ok"); + okBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + listener.onTextInput(textField.getText()); + closeDialog(); + } + }); + + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + layout.setHorizontalGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup() + .addComponent(lbl) + .addComponent(textField) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(ComponentPlacement.UNRELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(cancelBtn) + .addPreferredGap(ComponentPlacement.UNRELATED) + .addComponent(okBtn) + ) + ) + .addContainerGap()); + layout.setVerticalGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(lbl) + .addPreferredGap(ComponentPlacement.UNRELATED) + .addComponent(textField) + .addPreferredGap(ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup() + .addComponent(cancelBtn) + .addComponent(okBtn) + ) + .addContainerGap() + ); + + getRootPane().setDefaultButton(okBtn); + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Close Dialog"); + getRootPane().getActionMap().put("Close Dialog", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + closeDialog(); + } + }); + + getContentPane().add(panel); + pack(); + } + + protected void closeDialog() { + setVisible(false); + dispose(); + } + +} diff --git a/chunky/src/java/se/llbit/chunky/ui/TextInputListener.java b/chunky/src/java/se/llbit/chunky/ui/TextInputListener.java new file mode 100644 index 0000000000..e9f9413624 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/TextInputListener.java @@ -0,0 +1,5 @@ +package se.llbit.chunky.ui; + +public interface TextInputListener { + void onTextInput(String data); +} diff --git a/chunky/src/java/se/llbit/chunky/ui/TextOutputDialog.java b/chunky/src/java/se/llbit/chunky/ui/TextOutputDialog.java new file mode 100644 index 0000000000..6c28bc6782 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/ui/TextOutputDialog.java @@ -0,0 +1,123 @@ +/* Copyright (c) 2012 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.ui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.GroupLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.LayoutStyle.ComponentPlacement; + +/** + * Dedicated error reporting dialog. + * + * Used to display critical errors in a nicer way. + * + * @author Jesper Öqvist + */ +@SuppressWarnings("serial") +public class TextOutputDialog extends JDialog { + + /** + * Initialize the error dialog. + */ + public TextOutputDialog(String title, String message, String data) { + super(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setModalityType(ModalityType.APPLICATION_MODAL); + + setTitle(title); + setLocationRelativeTo(null); + + JPanel panel = new JPanel(); + JLabel lbl = new JLabel(message); + final JTextField textField = new JTextField(40); + textField.setText(data); + textField.addFocusListener(new FocusListener() { + + @Override + public void focusLost(FocusEvent e) { + } + + @Override + public void focusGained(FocusEvent e) { + textField.selectAll(); + } + }); + textField.selectAll(); + JButton closeBtn = new JButton("Close"); + closeBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + closeDialog(); + } + }); + + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + layout.setHorizontalGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup() + .addComponent(lbl) + .addComponent(textField) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(ComponentPlacement.UNRELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(closeBtn) + ) + ) + .addContainerGap()); + layout.setVerticalGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(lbl) + .addPreferredGap(ComponentPlacement.UNRELATED) + .addComponent(textField) + .addPreferredGap(ComponentPlacement.UNRELATED) + .addComponent(closeBtn) + .addContainerGap() + ); + + getRootPane().setDefaultButton(closeBtn); + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Close Dialog"); + getRootPane().getActionMap().put("Close Dialog", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + closeDialog(); + } + }); + + getContentPane().add(panel); + pack(); + } + + protected void closeDialog() { + setVisible(false); + dispose(); + } + +} diff --git a/chunky/src/java/se/llbit/chunky/world/Icon.java b/chunky/src/java/se/llbit/chunky/world/Icon.java index fe5a1e7169..3c7ff78395 100644 --- a/chunky/src/java/se/llbit/chunky/world/Icon.java +++ b/chunky/src/java/se/llbit/chunky/world/Icon.java @@ -41,6 +41,12 @@ public class Icon extends Texture { public static final Icon woodenDoor = new Icon("wooden-door"); public static final Icon woodenPressurePlate = new Icon("wooden-pressure-plate"); public static final Icon woodenStairs = new Icon("wooden-stairs"); + public static final Icon skyboxUp = new Icon("skybox-up"); + public static final Icon skyboxDown = new Icon("skybox-down"); + public static final Icon skyboxLeft = new Icon("skybox-left"); + public static final Icon skyboxRight = new Icon("skybox-right"); + public static final Icon skyboxFront = new Icon("skybox-front"); + public static final Icon skyboxBack = new Icon("skybox-back"); public static final Icon isoNE = new Icon("iso-ne"); public static final Icon isoWN = new Icon("iso-wn"); public static final Icon isoSW = new Icon("iso-sw"); @@ -53,10 +59,15 @@ public class Icon extends Texture { public static final Icon lock = new Icon("lock"); public static final Icon key = new Icon("key"); public static final Icon disk = new Icon("disk"); + public static final Icon load = new Icon("load"); + public static final Icon save = new Icon("save"); public static final Icon play = new Icon("play"); public static final Icon pause = new Icon("pause"); public static final Icon stop = new Icon("stop"); public static final Icon reload = new Icon("reload"); + public static final Icon colors = new Icon("colors"); + public static final Icon sky = new Icon("sky"); + public static final Icon camera = new Icon("camera"); public Icon(String resourceName) { setTexture(ImageLoader.get("icons/" + resourceName + ".png")); diff --git a/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java b/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java index be1e2f5ffe..861bd4ad80 100644 --- a/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java +++ b/chunky/src/java/se/llbit/chunky/world/SkymapTexture.java @@ -179,8 +179,8 @@ public float[] getColor(double u, double v) { @Override public void getColorInterpolated(double u, double v, Vector4d c) { - double x = u * width; - double y = v * (height-1); + double x = u * (width-1); + double y = (1-v) * (height-1); double weight; int fx = (int) QuickMath.floor(x); int cx = (int) QuickMath.ceil(x); @@ -188,22 +188,22 @@ public void getColorInterpolated(double u, double v, Vector4d c) { int cy = (int) QuickMath.ceil(y); double r, g, b; - getColor(fx % width, fy, c); + getColor(fx, fy, c); weight = (1 - (y-fy)) * (1 - (x-fx)); r = weight * c.x; g = weight * c.y; b = weight * c.z; - getColor(cx % width, fy, c); + getColor(cx, fy, c); weight = (1 - (y-fy)) * (1 - (cx-x)); r += weight * c.x; g += weight * c.y; b += weight * c.z; - getColor(fx % width, cy, c); + getColor(fx, cy, c); weight = (1 - (cy-y)) * (1 - (x-fx)); r += weight * c.x; g += weight * c.y; b += weight * c.z; - getColor(cx % width, cy, c); + getColor(cx, cy, c); weight = (1 - (cy-y)) * (1 - (cx-x)); r += weight * c.x; g += weight * c.y; diff --git a/chunky/src/java/se/llbit/math/Color.java b/chunky/src/java/se/llbit/math/Color.java index a97198cc01..b2a86e4aed 100644 --- a/chunky/src/java/se/llbit/math/Color.java +++ b/chunky/src/java/se/llbit/math/Color.java @@ -171,6 +171,17 @@ public static final void getRGBAComponents(int irgb, Vector4d v) { v.z = (0xFF & irgb) / 255.f; } + /** + * Get the RGBA color components from an INT ARGB value + * @param irgb + * @param v + */ + public static final void getRGBAComponents(int irgb, Vector3d v) { + v.x = (0xFF & (irgb >> 16)) / 255.f; + v.y = (0xFF & (irgb >> 8)) / 255.f; + v.z = (0xFF & irgb) / 255.f; + } + /** * Get the RGBA color components from an INT ARGB value * @param irgb @@ -241,4 +252,47 @@ public static void toLinear(float[] components) { components[i] = (float) FastMath.pow(components[i], Scene.DEFAULT_GAMMA); } } + + public static String toString(double r, double g, double b) { + int rgb = getRGB(r, g, b); + return String.format("%02X%02X%02X", (rgb>>16)&0xFF, (rgb>>8)&0xFF, rgb&0xFF); + } + + public static String toString(Vector3d color) { + int rgb = getRGB(color.x, color.y, color.z); + return String.format("%02X%02X%02X", (rgb>>16)&0xFF, (rgb>>8)&0xFF, rgb&0xFF); + } + + public static void RGBfromHSL(Vector3d rgb, double hue, + double saturation, double lightness) { + double c = Math.min(1, (1 - Math.abs(2*lightness - 1)) * saturation); + double h = hue*6; + double x = c * (1 - Math.abs(h%2 - 1)); + if (h < 1) { + rgb.set(c, x, 0); + } else if (h < 2) { + rgb.set(x, c, 0); + } else if (h < 3) { + rgb.set(0, c, x); + } else if (h < 4) { + rgb.set(0, x, c); + } else if (h < 5) { + rgb.set(x, 0, c); + } else { + rgb.set(c, 0, x); + } + double m = Math.max(0, lightness - 0.5*c); + rgb.x += m; + rgb.y += m; + rgb.z += m; + } + + public static java.awt.Color toAWT(Vector3d color) { + return new java.awt.Color((float)color.x, (float)color.y, (float)color.z); + } + + public static void fromString(String text, int radix, Vector3d color) throws NumberFormatException { + int rgb = Integer.parseInt(text, radix); + Color.getRGBAComponents(rgb, color); + } } diff --git a/chunky/src/java/se/llbit/math/Constants.java b/chunky/src/java/se/llbit/math/Constants.java new file mode 100644 index 0000000000..b0fa36242b --- /dev/null +++ b/chunky/src/java/se/llbit/math/Constants.java @@ -0,0 +1,25 @@ +/* Copyright (c) 2014 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.math; + +public class Constants { + public static final double HALF_PI = Math.PI / 2; + // TODO INV_TAU + public static final double TAU = Math.PI * 2; + public static final double SQRT_HALF = Math.sqrt(0.5); + public static final double INV_SQRT_HALF = 1 / Math.sqrt(0.5); +} diff --git a/chunky/src/java/se/llbit/math/Vector3d.java b/chunky/src/java/se/llbit/math/Vector3d.java index d1f4cdd99a..dba7c45175 100644 --- a/chunky/src/java/se/llbit/math/Vector3d.java +++ b/chunky/src/java/se/llbit/math/Vector3d.java @@ -18,6 +18,8 @@ import org.apache.commons.math3.util.FastMath; +import se.llbit.json.JsonObject; + /** * A 3D vector of doubles. * @author Jesper Öqvist @@ -144,6 +146,18 @@ public final void scaleAdd(double s, Vector3d d, Vector3d o) { z = s*d.z + o.z; } + /** + * Add s*d to this vector + * @param s + * @param d + * @param o + */ + public final void scaleAdd(double s, Vector3d d) { + x += s*d.x; + y += s*d.y; + z += s*d.z; + } + /** * Scale this vector by s * @param s @@ -233,4 +247,26 @@ public void set(Vector3i a) { public String toString() { return String.format("(%f, %f, %f)", x, y, z); } + + /** + * De-serialize + * @param object + */ + public void fromJson(JsonObject object) { + x = object.get("x").doubleValue(0); + y = object.get("y").doubleValue(0); + z = object.get("z").doubleValue(0); + } + + /** + * Serialize + * @return JSON object + */ + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.add("x", x); + object.add("y", y); + object.add("z", z); + return object; + } } diff --git a/chunky/src/java/se/llbit/math/Vector4d.java b/chunky/src/java/se/llbit/math/Vector4d.java index 33889ab4cd..9c8a85a09d 100644 --- a/chunky/src/java/se/llbit/math/Vector4d.java +++ b/chunky/src/java/se/llbit/math/Vector4d.java @@ -32,6 +32,13 @@ public Vector4d() { this(0, 0, 0, 0); } + /** + * Copy constructor + */ + public Vector4d(Vector4d v) { + this(v.x, v.y, v.z, v.w); + } + /** * @param i * @param j @@ -91,4 +98,16 @@ public void set(float[] v) { z = v[2]; w = v[3]; } + + /** + * Scale and add argument the vector + * @param s + * @param v + */ + public void scaleAdd(double s, Vector4d v) { + x += s*v.x; + y += s*v.y; + z += s*v.z; + w += s*v.w; + } } diff --git a/chunky/src/res/Version.properties b/chunky/src/res/Version.properties index e4220dead9..6573cd912f 100644 --- a/chunky/src/res/Version.properties +++ b/chunky/src/res/Version.properties @@ -1,2 +1,2 @@ -#Sun, 13 Jul 2014 20:50:24 +0200 -version=1.2.3 +#Mon, 21 Jul 2014 10:20:40 +0200 +version=1.2.3-33-gf0c6eee diff --git a/chunky/src/res/icons/camera.png b/chunky/src/res/icons/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..c4901350d8f8b7bb69ce0bcbf9ffdd070ba9dc0f GIT binary patch literal 557 zcmV+|0@D47P)D`~Chc=5;{Q@AuJl{Vk>2 z?Z$e&e!~46kT{M5fDnSgV1Va&0G!Wf4u=C$N&r@?Rf^SKH&-eZ#^W*b`5fQ(3BwQ} z1oe8ID2niXpU30DcDn`8Qm(qLBZOc$95S6w+3j}ZayeS97WsT0&-1w7?+78DBx*on z+cuVE(dl$hO0nDRm`ob=--+FYPA~2|jKU|PfrXQI~2e@1=0Gv)Igb-=}U8B*6 zX0wS90?V=hP)a@7m^hAurfD=94Lr}oFpQLTwOYmZeE>3<3~?N@*=(L&xZQ3@DT$(p zVzG!)3ez;vG>sq#0LW&u#Bt2y@pzlWq}S`ELCWXz#Bq$263eoXQgXlF0n+VnqulLw vKSudyfc|<+?RGo0N-2fwy8m=c|9#G1Uh~Db%--e300000NkvXXu0mjf>dg0Y literal 0 HcmV?d00001 diff --git a/chunky/src/res/icons/colors.png b/chunky/src/res/icons/colors.png new file mode 100644 index 0000000000000000000000000000000000000000..803747abce9d2a1e2eae39c4fcd6c3c3acc39f6e GIT binary patch literal 525 zcmV+o0`mQdP)M4hcRS3Pi0+~(t(1b=48XwoK?-|)53|~e|K5`lI0^Oo8EXkH+EB`+c*U1WG zd}NTEr?&!1e-O6dDRd8yr_7ZSnFr@ zqIBnAMpABoXW9vlae?+(KmbVvX~B_lW&lUZzG-(|Uc}vGX}4csS?_?n$MyzraT-x1d9n&YgKGRt7#Lmw87Sx#MII>oAD5{$+n?IpUW`s%=gaw4Ko)0 za;CLi0NlBK6NkgWo>PqoA*e}w>#V7hSj-g}9sfeO?*(#oF+e(<=H-LecrQ1j?A^x3Ll#pCa;4#0{41~y zi5h5{#Yo9-TKEL?RJ1O{24?gO=`nWKtQ!AQ4Yslh&fD zDgfapA(~p;IHb)L14U6#6oso@8B((;tdg~IdFl@m>Q7UvkM0dHIPlb*AQ%jCzpJ0l zo(?v6CAKzfVR~vBNtaPol~5=IfY0aS!{`DJ{lU^R?)49grPRAOiRQi?!sT&_TJ-vd zMdR+nVp)PR2`nD(Igv~z`CtA3*h(yJ)(dL|00000NkvXX Hu0mjfLwgF{ literal 0 HcmV?d00001 diff --git a/chunky/src/res/icons/save.png b/chunky/src/res/icons/save.png new file mode 100644 index 0000000000000000000000000000000000000000..11fb52d7b06d5ed45c537cc658ae972b939a9a2d GIT binary patch literal 660 zcmV;F0&D$=P)qG;Gfq7sZQ1i_0UNHW2Q ziX^%TB+DSW>%yR@WDQmqnKex#Wf~G}5M%9}>E<@q>7tf9Zb;9~2jBBP&-?L%&%j@` zw03F$x2|2pXf%>er-{X4n9XLA$s_=YM1o8v!_7MnvoXYMcz;6&0P%R7U@%CoJRg@Z z2mo}`hCTf~SX)49OUEAq9bBI(j#oZPc z>95no-@nss+f%a>{L@@-mANoH%3zj3NoIxS#4_U6a*pq?q4>uO!kn6#;l<)2X+_aP zjQ!F8Xf&Bvo}1N$qKeAQ%sj7CNirrQ-A|uUU0uzSm%-J7jRyF+X4~BSH$tH>UO_-8 zDqJxSYm)gF09>FRPPwi0a=zI z=gjKWxc^VVQZj+6$Af$s%h uU^u$hs;N1^LB=v5l1&w%>)=s$i#wJ)CJQJGARHa zUf%G!;dp{%%;0=?6+?*cnAZ(CzOA9hX`#Ge4D$U~hAyl3m?fHSe zt~YfbrIe0BDW$7TM+>FYz##Qsh~nKlu8-sTUlHe@ aFVhDOyPNX&>`LMQ00005(ej@)Wnk1 z6ovB4k_-iRPv3y>Mm}+%q99Kf#}JFt$#-^qG-Fn6V4eB+`SPaFi_3h=JB%9|fnegq zkM>6YfuQF{%z+~giVJQq90T%#pRl^Iv9KXvVDCz&i8s^W{yU7nk{#cNjM`0>Q+I zAMK6)13}M^m;*-~6c^lJI0obeKVfxaV{7}*{HoPaqQgBUHIOAo&OS{hGtr?OXl~X@ W>EJkN>sLS<7(8A5T-G@yGywoOe@jUK literal 0 HcmV?d00001 diff --git a/chunky/src/res/icons/skybox-front.png b/chunky/src/res/icons/skybox-front.png new file mode 100644 index 0000000000000000000000000000000000000000..c0133860f9618a60f4745c8b8a5e15a7fd4a95a2 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)0hmWEf<^PV4IfMK}vQ zB8wRqxP?KOkzv*x37{Z*iKnkC`#p9s0S+cfXYU_CA;}Wgh!W@g+}zZ>5(ej@)Wnk1 z6ovB4k_-iRPv3y>Mm}+%q5w}9#}JFt$#-^qG-Fn6V4eB+`SPaFi_3h=JB%9|fnegq zkM>6YfuQF{%z+~giVJQq90T%#pRkIR@V2L>e2}vEaVX&dbD>2W5(ej@)Wnk1 z6ovB4k_-iRPv3y>Mm}+%qCig<#}JFt$#-^qG-Fn6V4eB+`SPaFi_3h=JB%9|fnegq zkM>6YfuQF{%z+~giVJQq90T%#pRi6--n8le$6F0)4<2wgH}W5OD8W`z&d3nxEgQC; SbKOs%;S8RxelF{r5}E+b{!M}a literal 0 HcmV?d00001 diff --git a/chunky/src/res/icons/skybox-right.png b/chunky/src/res/icons/skybox-right.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c28cfd69af842a0ad04fa9d6ac4760cd890788 GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)0hmWEf<^PV4IfMK}vQ zB8wRqxP?KOkzv*x37{Z*iKnkC`#p9s0d}JuOzE?MLXst}5hc#~xw)x%B@E6*sfi`2 zDGKG8B^e6tp1uL$jeO!jMd6+KXvVDCz&i8s^W{yU7nk{#cNjM`0>Q+I zAMK6)13}M^m;*-~6c^lJI0obeKVfxaV{7~WLqulUM8}7>?JA8<97=e=Txik8csG`T Zp;$;R_}bp5#y~3=JYD@<);T3K0RX=iPc8re literal 0 HcmV?d00001 diff --git a/chunky/src/res/icons/skybox-up.png b/chunky/src/res/icons/skybox-up.png new file mode 100644 index 0000000000000000000000000000000000000000..462874315de8cd28c5dab65feddfdec8ee90c4fa GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)0hmWEf<^PV4IfMK}vQ zB8wRqxP?KOkzv*x37{Z*iKnkC`#p9s0S>{rf4S6vLXst}5hc#~xw)x%B@E6*sfi`2 zDGKG8B^e6tp1uL$jeO!jMPZ&Ujv*GOlke>KXvVDCz&i8s^W{yU7nk{#cNjM`Ha0%& zSo5YnW#8Gy$9sOn95~{jxZnoEvH$=72R~tTV`FRk&-|*@QKF-#r$>VcD4H&lndnds ZG`A{JI(TPqbtTXS22WQ%mvv4FO#o4LP1*ne literal 0 HcmV?d00001 diff --git a/lib/jastadd/Json.jrag b/lib/jastadd/Json.jrag index 7ae3797be9..8d2117e09f 100644 --- a/lib/jastadd/Json.jrag +++ b/lib/jastadd/Json.jrag @@ -155,9 +155,73 @@ aspect Json { eq JsonTrue.boolValue(boolean undefined) = true; eq JsonFalse.boolValue(boolean undefined) = false; + /** + * Produce a compact string representation of this JSON object. + * @return compact string + */ + public abstract String JsonValue.toCompactString(); + + public String JsonUnknown.toCompactString() { + return "\"\""; + } + + public String JsonMember.toCompactString() { + return "\"" + getName() + "\":" + getValue().toCompactString(); + } + + public String JsonTrue.toCompactString() { + return "true"; + } + + public String JsonFalse.toCompactString() { + return "false"; + } + + public String JsonNull.toCompactString() { + return "null"; + } + + public String JsonNumber.toCompactString() { + return getValue(); + } + + public String JsonString.toCompactString() { + return "\"" + getValue() + "\""; + } + + public String JsonObject.toCompactString() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + boolean first = true; + for (JsonMember member: getMemberList()) { + if (!first) { + sb.append(","); + } + first = false; + sb.append(member.toCompactString()); + } + sb.append("}"); + return sb.toString(); + } + + public String JsonArray.toCompactString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + boolean first = true; + for (JsonValue element: getElementList()) { + if (!first) { + sb.append(","); + } + first = false; + sb.append(element.toCompactString()); + } + sb.append("]"); + return sb.toString(); + } + @Override public String JsonMember.toString() { - return getName() + " : " + getValue().toString(); + return "\"" + getName() + "\" : " + getValue().toString(); } @Override diff --git a/renderblocks.sh b/misc/renderblocks.sh similarity index 100% rename from renderblocks.sh rename to misc/renderblocks.sh diff --git a/test.sh b/misc/test.sh similarity index 100% rename from test.sh rename to misc/test.sh diff --git a/releasebot.py b/releasebot.py index 72d59b8c04..f7f3d37ac9 100644 --- a/releasebot.py +++ b/releasebot.py @@ -21,6 +21,37 @@ from string import join from launchpadlib.launchpad import Launchpad from os import path +from shutil import copyfile + +class Credentials: + credentials = {} + + def __init__(self): + if path.exists('credentials.json'): + with open('credentials.json', 'r') as fp: + self.credentials = json.load(fp) + + def get(self, key): + if key not in self.credentials: + self.credentials[key] = raw_input(key+': ') + self.save() + return self.credentials[key] + + def getpass(self, key): + if key not in self.credentials: + self.credentials[key] = getpass(prompt=key+': ') + self.save() + return self.credentials[key] + + def remove(self, key): + del self.credentials[key] + self.save() + + def save(self): + with open('credentials.json', 'w') as fp: + json.dump(self.credentials, fp) + + class Version: regex = re.compile('^(\d+\.\d+\.\d+)-?([a-zA-Z]*\.?\d*)$') @@ -28,29 +59,36 @@ class Version: milestone = '' suffix = '' series = '' - rc = '' changelog = '' release_notes = '' def __init__(self, version): self.full = version - regex = re.compile('^(\d+\.\d+\.\d+)-?([a-zA-Z]*\.?\d*)$') + regex = re.compile('^(\d+\.\d+(\.\d+)?)-?([a-zA-Z]*\.?\d*)?$') r = regex.match(version) - assert r, "Invalid version name: %s (expected e.g. 1.2.13-rc.1)" % version + assert r, "Invalid version name: %s (expected e.g. 1.2.13-alpha1)" % version self.milestone = r.groups()[0] - self.suffix = r.groups()[1] + self.suffix = r.groups()[2] self.series = join(self.milestone.split('.')[:2], '.') - if self.suffix.startswith('rc.'): - self.rc = self.suffix[3:] - else: - notes_fn = "release_notes-%s.txt" % self.milestone - try: - with codecs.open(notes_fn, 'r', encoding='utf-8') as f: - self.release_notes = f.read().replace('\r', '') - except: + notes_fn = 'release_notes-%s.txt' % self.milestone + notes_fn2 = 'release_notes-%s.txt' % self.full + if not path.exists(notes_fn): + if path.exists(notes_fn2): + notes_fn = notes_fn2 + else: print "Error: release notes not found!" print "Please edit release_notes-%s.txt!" % self.milestone sys.exit(1) + if not path.exists(notes_fn2): + copyfile(notes_fn, notes_fn2) + else: + notes_fn = notes_fn2 + try: + with codecs.open(notes_fn, 'r', encoding='utf-8') as f: + self.release_notes = f.read().replace('\r', '') + except: + print "Error: failed to read release notes!" + sys.exit(1) try: # load changelog @@ -68,42 +106,86 @@ def __init__(self, version): print "Error: ChangeLog is empty!" sys.exit(1) -def publish(version): +def build_release(version): if raw_input('Build release? [y/n] ') == "y": if call(['cmd', '/c', 'ant', '-Dversion=' + version.full, 'release']) is not 0: print "Error: Ant build failed!" sys.exit(1) if call(['makensis', 'Chunky.nsi']) is not 0: - print "Error: NSIS failed!" + print "Error: NSIS build failed!" + sys.exit(1) + if raw_input('Publish to Launchpad? [y/n] ') == "y": + (is_new, exe, zip, jar) = publish_launchpad(version) + patch_url(version, jar) + write_release_notes(version, exe, zip) + if raw_input('Publish to FTP? [y/n] ') == "y": + publish_ftp(version) + if raw_input('Post release thread? [y/n] ') == "y": + post_release_thread(version) + if raw_input('Update documentation? [y/n] ') == "y": + update_docs(version) + +def build_snapshot(version): + if raw_input('Build snapshot? [y/n] ') == "y": + if call(['cmd', '/c', 'git', 'tag', '-a', version.full, '-m', 'Snapshot build']) is not 0: + print "Error: git tag failed!" + sys.exit(1) + if call(['cmd', '/c', 'ant', '-Ddebug=true', 'dist']) is not 0: + print "Error: Ant build failed!" sys.exit(1) - if not version.rc: - if raw_input('Publish files? [y/n] ') == "y": - (is_new, exe, zip, jar) = publish_release(version) - patch_url(version, jar) - write_markup(version, exe, zip) - if raw_input('Post release thread? [y/n] ') == "y": - post_release_thread(version) - if raw_input('Upload latest.json? [y/n] ') == "y": - ftpupload(version) - if raw_input('Update documentation? [y/n] ') == "y": - update_docs(version) - -def ftpupload(version): + if raw_input('Publish snapshot to FTP? [y/n] ') == "y": + publish_snapshot_ftp(version) + if raw_input('Post snapshot thread? [y/n] ') == "y": + post_snapshot_thread(version) + +def reddit_login(): while True: - user = raw_input('ftp user: ') - pw = getpass(prompt='ftp password: ') + user = credentials.get('reddit user') + pw = credentials.getpass('reddit password') + try: + r = praw.Reddit(user_agent=user) + r.login('releasebot', pw) + return r + except praw.errors.InvalidUserPass: + credentials.remove('reddit user') + credentials.remove('reddit password') + print "Login failed, please try again" + +def ftp_login(): + while True: + user = credentials.get('ftp user') + pw = credentials.getpass('ftp password') try: ftp = ftplib.FTP('ftp.llbit.se') ftp.login(user, pw) - break + return ftp except ftplib.error_perm: + credentials.remove('ftp user') + credentials.remove('ftp password') print "Login failed, please try again" + +def publish_snapshot_ftp(version): + ftp = ftp_login() ftp.cwd('chunkyupdate') + with open('build/ChunkyLauncher.jar', 'rb') as f: + ftp.storbinary('STOR ChunkyLauncher.jar', f) + with open('latest.json', 'rb') as f: + ftp.storbinary('STOR snapshot.json', f) + ftp.cwd('lib') + with open('build/chunky-core-%s.jar' % version.full, 'rb') as f: + ftp.storbinary('STOR chunky-core-%s.jar' % version.full, f) + ftp.quit() + +def publish_ftp(version): + ftp = ftp_login() + ftp.cwd('chunkyupdate') + with open('build/ChunkyLauncher.jar', 'rb') as f: + ftp.storbinary('STOR ChunkyLauncher.jar', f) with open('latest.json', 'rb') as f: ftp.storbinary('STOR latest.json', f) ftp.cwd('lib') - with open('build/chunky-core-%s.jar' % version.milestone, 'rb') as f: - ftp.storbinary('STOR chunky-core-%s.jar' % version.milestone, f) + with open('build/chunky-core-%s.jar' % version.full, 'rb') as f: + ftp.storbinary('STOR chunky-core-%s.jar' % version.full, f) ftp.quit() def update_docs(version): @@ -143,7 +225,7 @@ def lp_upload_file(version, release, filename, description, content_type, file_t traceback.print_exception(exc_type, exc_value, exc_traceback) return None -def publish_release(version): +def publish_launchpad(version): if raw_input('Publish to production? [y/n] ') == "y": server = 'production' else: @@ -239,7 +321,7 @@ def publish_release(version): return (is_new_release, exe_url, zip_url, jar_url) "output markdown" -def write_markup(version, exe_url, zip_url): +def write_release_notes(version, exe_url, zip_url): text = '''###Downloads * [Windows installer](%s) @@ -270,14 +352,35 @@ def post_release_thread(version): except IOError: print "Error: reddit post must be in build/release_notes-%s.md" % version.milestone return - r = praw.Reddit(user_agent='releasebot') - pw = getpass(prompt='releasebot login: ') - r.login('releasebot', pw) + r = reddit_login() post = r.submit('chunky', 'Chunky %s released!' % version.full, text=text) post.set_flair('announcement', 'announcement') post.sticky() - print "Submitted Reddit release thread!" + print "Submitted release thread!" + +"post reddit release thread" +def post_snapshot_thread(version): + r = reddit_login() + post = r.submit('chunky', 'Chunky Snapshot %s' % version.full, + text='''###Snapshot %s + +A new snapshot for Chunky is now available. The snapshot is mostly untested, +so please make sure to backup your scenes before using it. + +[The snapshot can be downloaded using the launcher.](http://chunky.llbit.se/snapshot.html) + +###Notes + +*These are preliminary release notes for upcoming features (which may not be fully functional).* + +%s + +###ChangeLog + +''' % (version.full, version.release_notes) + version.changelog) + post.set_flair('announcement', 'announcement') + print "Submitted snapshot thread!" "patch url into latest.json" def patch_url(version, url): @@ -304,45 +407,55 @@ def patch_url(version, url): ### MAIN version = None +options = {'ftp': False, 'docs': False, 'snapshot': False} do_ftpupload = False do_update_docs = False for arg in sys.argv[1:]: if arg == '-h' or arg == '--h' or arg == '-help' or arg == '--help': print "usage: releasebot [VERSION] [COMMAND]" print "commands:" - print "-ftp upload latest.json to FTP server" + print " -ftp upload latest.json to FTP server" + print " -docs update documentation" + print " -snapshot build snapshot instead of release" print print "This utility creates a new release of Chunky" - print "Required Python libraries: launchpadlib, praw" - print "Upgrade with pip install --upgrade PKG" + print "Required Python libraries: launchpadlib, PRAW" + print "Upgrade with >pip install --upgrade " sys.exit(0) - elif arg == '-ftp': - do_ftpupload = True - elif arg == '-docs': - do_update_docs = True else: - version = Version(arg) - -if version == None: - version = Version(raw_input('Enter version: ')) - -if do_ftpupload: - ftpupload(version) - sys.exit(0) - -if do_update_docs: - update_docs(version) - sys.exit(0) + matched = False + for key in options.keys(): + if arg == '-'+key: + options[key] = True + matched = True + break + if not matched: + version = Version(arg) -print "Ready to build version %s!" % version.full try: - publish(version) - if raw_input('All done. Push git changes? [y/n] ') == "y": - call(['git', 'push', 'origin', 'master'])# push version bump commit - call(['git', 'push', 'origin', version.full])# push version tag + credentials = Credentials() + + if version == None: + version = Version(raw_input('Enter version: ')) + + if options['ftp']: + publish_ftp(version) + elif options['docs']: + update_docs(version) + elif options['snapshot']: + print "Ready to build snapshot %s!" % version.full + build_snapshot(version) + else: + print "Ready to build version %s!" % version.full + build_release(version) + if raw_input('Push git release commit? [y/n] ') == "y": + call(['git', 'push', 'origin', 'master'])# push version bump commit + call(['git', 'push', 'origin', version.full])# push version tag + print "All done." except: exc_type, exc_value, exc_traceback = sys.exc_info() print "Unexpected error:" traceback.print_exception(exc_type, exc_value, exc_traceback) print "Release aborted." raw_input() +