/* Copyright (c) 2012-2014 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.renderer.scene; import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.resources.HDRTexture; import se.llbit.chunky.resources.PFMTexture; import se.llbit.chunky.resources.Texture; import se.llbit.chunky.world.Block; import se.llbit.chunky.world.Clouds; import se.llbit.chunky.world.SkymapTexture; import se.llbit.json.Json; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; import se.llbit.log.Log; import se.llbit.math.ColorUtil; import se.llbit.math.Constants; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.math.Vector4; import se.llbit.resources.ImageLoader; import se.llbit.util.JsonSerializable; import se.llbit.util.NotNull; 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 java.util.stream.Collectors; /** * Sky model and sky state for ray tracing. * * @author Jesper Öqvist <jesper@llbit.se> */ public class Sky implements JsonSerializable { //private static final double CLOUD_OPACITY = 0.4; /** * 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.0; 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; // TODO(jesper): add simulated night-time mode. /** * Sky rendering mode * * @author Jesper Öqvist <jesper@llbit.se> */ public enum SkyMode { /** * Use simulated sky. */ SIMULATED("Simulated"), /** * Use a gradient. */ GRADIENT("Color Gradient"), /** * Use a panormaic skymap. */ SKYMAP_PANORAMIC("Skymap (panoramic)"), /** * Light probe. */ SKYMAP_SPHERICAL("Skymap (spherical)"), /** * Use a skybox. */ SKYBOX("Skybox"), /** * Render a completely black sky, useful for rendering an emitter-only pass. */ BLACK("Black"); private String name; SkyMode(String name) { this.name = name; } @Override public String toString() { return name; } public static final SkyMode DEFAULT = SIMULATED; public static final SkyMode[] values = values(); public static SkyMode get(String name) { try { return SkyMode.valueOf(name); } catch (IllegalArgumentException e) { return DEFAULT; } } } @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 Scene 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 Vector3 cloudOffset = new Vector3(0, DEFAULT_CLOUD_HEIGHT, 0); private double skyLightModifier = DEFAULT_INTENSITY; private List<Vector4> gradient = new LinkedList<>(); /** * Current rendering mode */ private SkyMode mode = SkyMode.DEFAULT; public Sky(Scene sceneDescription) { this.scene = sceneDescription; makeDefaultGradient(gradient); } /** * Load the configured skymap file */ 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; } } /** * Load a panoramic skymap texture. */ public void loadSkymap(String fileName) { skymapFileName = fileName; skymap = loadSkyTexture(fileName, skymap); scene.refresh(); } /** * Set the sky equal to other sky. */ 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; 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]; } } /** * Calculate sky color for the ray, based on sky mode. */ public void getSkyDiffuseColorInner(Ray ray) { 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; Vector4 c0 = gradient.get(x); Vector4 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 * len); 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; } case BLACK: { ray.color.set(0, 0, 0, 1); break; } } } /** * Panoramic skymap color. */ public void getSkyColor(Ray ray) { getSkyDiffuseColorInner(ray); ray.color.scale(skyLightModifier); ray.color.w = 1; } /** * Bilinear interpolated panoramic skymap color. */ public void getSkyColorInterpolated(Ray 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; } 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 * len); 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); } 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); } } if (scene.sunEnabled) { addSunColor(ray); } //ray.color.scale(skyLightModifier); ray.color.w = 1; } /** * Get the specular sky color for the ray. */ public void getSkySpecularColor(Ray ray) { getSkyColor(ray); 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. */ 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; } } /** * Set the polar offset of the skymap. */ public void setRotation(double value) { rotation = value; scene.refresh(); } /** * @return The polar offset of the skymap */ public double getRotation() { return rotation; } /** * Set sky mirroring at the horizon */ public void setMirrored(boolean b) { if (b != mirrored) { mirrored = b; scene.refresh(); } } /** * @return <code>true</code> if the sky is mirrored at the horizon */ public boolean isMirrored() { return mirrored; } /** * Set the sky rendering 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(); } } /** * @return Current sky rendering mode */ public SkyMode getSkyMode() { return mode; } @Override public JsonObject toJson() { JsonObject sky = new JsonObject(); sky.add("skyYaw", rotation); sky.add("skyMirrored", mirrored); 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(Json.NULL); } } sky.add("skybox", array); break; } default: { break; } } return sky; } public void importFromJson(JsonObject json) { rotation = json.get("skyYaw").doubleValue(rotation); mirrored = json.get("skyMirrored").boolValue(mirrored); skyLightModifier = json.get("skyLight").doubleValue(skyLightModifier); mode = SkyMode.get(json.get("mode").stringValue(mode.name())); horizonOffset = json.get("horizonOffset").doubleValue(horizonOffset); cloudsEnabled = json.get("cloudsEnabled").boolValue(cloudsEnabled); cloudSize = json.get("cloudSize").doubleValue(cloudSize); if (json.get("cloudOffset").isObject()) { cloudOffset.fromJson(json.get("cloudOffset").object()); } if (json.get("gradient").isArray()) { List<Vector4> theGradient = gradientFromJson(json.get("gradient").array()); if (theGradient != null && theGradient.size() >= 2) { gradient = theGradient; } } switch (mode) { case SKYMAP_PANORAMIC: { skymapFileName = json.get("skymap").stringValue(skymapFileName); if (skymapFileName.isEmpty()) { skymapFileName = json.get("skymapFileName").stringValue(skymapFileName); } break; } case SKYBOX: { JsonArray array = json.get("skybox").array(); for (int i = 0; i < 6; ++i) { JsonValue value = array.get(i); skyboxFileName[i] = value.stringValue(skyboxFileName[i]); } break; } default: break; } } /** * Set the sky light modifier. */ public void setSkyLight(double newValue) { skyLightModifier = newValue; scene.refresh(); } /** * @return Current sky light modifier */ public double getSkyLight() { return skyLightModifier; } public void setGradient(List<Vector4> newGradient) { gradient = newGradient.stream().map(Vector4::new).collect(Collectors.toList()); scene.refresh(); } public List<Vector4> getGradient() { return gradient.stream().map(Vector4::new).collect(Collectors.toList()); } public static JsonArray gradientJson(Collection<Vector4> gradient) { JsonArray array = new JsonArray(); for (Vector4 stop : gradient) { JsonObject obj = new JsonObject(); obj.add("rgb", ColorUtil.toString(stop.x, stop.y, stop.z)); obj.add("pos", stop.w); array.add(obj); } return array; } /** * @return {@code null} if the gradient was not valid */ public static List<Vector4> gradientFromJson(JsonArray array) { List<Vector4> gradient = new ArrayList<>(array.size()); for (int i = 0; i < array.size(); ++i) { JsonObject obj = array.get(i).object(); Vector3 color = new Vector3(); try { ColorUtil.fromString(obj.get("rgb").stringValue(""), 16, color); Vector4 stop = new Vector4(color.x, color.y, color.z, obj.get("pos").doubleValue(Double.NaN)); if (!Double.isNaN(stop.w)) { gradient.add(stop); } } catch (NumberFormatException e) { // Ignored. } } boolean errors = false; for (int i = 0; i < gradient.size(); ++i) { Vector4 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<Vector4> gradient) { gradient.add(new Vector4(0x0B / 255., 0xAB / 255., 0xC7 / 255., 0)); gradient.add(new Vector4(0x75 / 255., 0xAA / 255., 0xFF / 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 { Log.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(ImageLoader.read(textureFile)); } } catch (IOException e) { Log.warn("Could not load skymap: " + fileName); } catch (Throwable e) { Log.error("Unexpected exception occurred!", e); } } else { Log.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 */ 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. */ public void setCloudsEnabled(boolean newValue) { if (newValue != cloudsEnabled) { cloudsEnabled = newValue; scene.refresh(); } } /** * @return <code>true</code> if cloud rendering is enabled */ public boolean cloudsEnabled() { return cloudsEnabled; } public boolean cloudIntersection(Scene scene, Ray ray) { double ox = ray.o.x + scene.origin.x; double oy = ray.o.y + scene.origin.y; double oz = ray.o.z + scene.origin.z; double offsetX = cloudOffset.x; double offsetY = cloudOffset.y; double offsetZ = cloudOffset.z; double inv_size = 1 / cloudSize; double cloudTop = offsetY + 5; int target = 1; double t_offset = 0; if (oy < offsetY || oy > cloudTop) { if (ray.d.y > 0) { t_offset = (offsetY - oy) / ray.d.y; } else { t_offset = (cloudTop - oy) / ray.d.y; } if (t_offset < 0) { return false; } // Ray is entering cloud. if (inCloud((ray.d.x * t_offset + ox) * inv_size + offsetX, (ray.d.z * t_offset + oz) * inv_size + offsetZ)) { ray.n.set(0, -Math.signum(ray.d.y), 0); onCloudEnter(ray, t_offset); return true; } } else if (inCloud(ox * inv_size + offsetX, oz * inv_size + offsetZ)) { target = 0; } double tExit; if (ray.d.y > 0) { tExit = (cloudTop - oy) / ray.d.y - t_offset; } else { tExit = (offsetY - oy) / ray.d.y - t_offset; } if (ray.t < tExit) { tExit = ray.t; } double x0 = (ox + ray.d.x * t_offset) * inv_size + offsetX; double z0 = (oz + 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); int xo = (1 + xmod) / 2, zo = (1 + zmod) / 2; 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 + xo - xp); double zlimit = xrem * m; while (t < tExit) { double zrem = zmod * (iz + zo - zp); 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; zp = z0 + zmod * i * m; } } else { double m = dx / dz; double zrem = zmod * (iz + zo - zp); double xlimit = zrem * m; while (t < tExit) { double xrem = xmod * (ix + xo - xp); 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; xp = x0 + xmod * i * m; } } int ny = 0; if (target == 1) { if (t > tExit) { return false; } ray.n.set(nx, ny, nz); onCloudEnter(ray, t + t_offset); return true; } else { if (t > tExit) { nx = 0; ny = (int) Math.signum(ray.d.y); nz = 0; t = tExit; } else { nx = -nx; nz = -nz; } ray.n.set(nx, ny, nz); onCloudExit(ray, t); } return true; } private static void onCloudEnter(Ray ray, double t) { ray.t = t; ray.color.set(1, 1, 1, 1); ray.setPrevMaterial(Block.AIR, 0); ray.setCurrentMaterial(Block.get(Block.STONE_ID), 0); // TODO add Cloud material } private static void onCloudExit(Ray ray, double t) { ray.t = t; ray.color.set(1, 1, 1, 1); ray.setPrevMaterial(Block.get(Block.STONE_ID), 0); ray.setCurrentMaterial(Block.AIR, 0); // TODO add Cloud material } private static boolean inCloud(double x, double z) { return Clouds.getCloud((int) Math.floor(x), (int) Math.floor(z)) == 1; } }