Java tutorial
/* 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 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.Random; import javax.imageio.ImageIO; 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.JsonArray; import se.llbit.json.JsonNull; import se.llbit.json.JsonObject; import se.llbit.json.JsonValue; import se.llbit.log.Log; 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 * @author Jesper qvist <jesper@llbit.se> */ public class Sky implements JSONifiable { //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; /** * Sky rendering mode * @author Jesper qvist <jesper@llbit.se> */ 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 (panoramic)"), /** * Light probe */ SKYMAP_SPHERICAL("Skymap (spherical)"), /** * Use a skybox */ SKYBOX("Skybox"); 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) { for (SkyMode mode : values) { if (mode.name().equals(name)) { return mode; } } 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 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; private List<Vector4d> gradient = new LinkedList<Vector4d>(); /** * Current rendering mode */ 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() { 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 * @param fileName */ public void loadSkymap(String fileName) { skymapFileName = fileName; skymap = loadSkyTexture(fileName, skymap); scene.refresh(); } /** * Set the sky equal to other sky * @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; skyLightModifier = other.skyLightModifier; gradient = new ArrayList<Vector4d>(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 * @param ray */ 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; 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 * 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; } default: break; } } /** * Panormaic skymap color * @param ray */ public void getSkyColor(Ray ray) { getSkyDiffuseColorInner(ray); ray.color.scale(skyLightModifier); ray.color.w = 1; } /** * Bilinear interpolated panoramic skymap color * @param ray */ 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 * @param 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. * @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; } } /** * Set the polar offset of the skymap * @param value */ 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 * @param b */ 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 * @param newMode */ 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(new JsonNull()); } } sky.add("skybox", array); break; } default: break; } return sky; } @Override 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<Vector4d> 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<Vector4d> newGradient) { gradient = new ArrayList<Vector4d>(newGradient.size()); for (Vector4d stop : newGradient) { gradient.add(new Vector4d(stop)); } scene.refresh(); } public List<Vector4d> getGradient() { List<Vector4d> copy = new ArrayList<Vector4d>(gradient.size()); for (Vector4d stop : gradient) { copy.add(new Vector4d(stop)); } return copy; } public static JsonArray gradientJson(Collection<Vector4d> 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<Vector4d> gradientFromJson(JsonArray array) { List<Vector4d> gradient = new ArrayList<Vector4d>(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<Vector4d> 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 { 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(ImageIO.read(textureFile)); } } catch (IOException e) { Log.warn("Could not load skymap: " + fileName); } catch (Throwable e) { Log.error("Unexpected exception ocurred!", 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 * @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(); } } /** * @return <code>true</code> if cloud rendering is enabled */ public boolean cloudsEnabled() { return cloudsEnabled; } public boolean cloudIntersection(Scene scene, Ray ray, Random random) { double offsetX = cloudOffset.x; double offsetY = cloudOffset.y; double offsetZ = cloudOffset.z; double inv_size = 1 / cloudSize; double cloudBot = offsetY - scene.origin.y; double cloudTop = offsetY - scene.origin.y + 5; int target = 1; double t_offset = 0; if (ray.o.y < cloudBot || ray.o.y > cloudTop) { if (ray.d.y > 0) { t_offset = (cloudBot - ray.o.y) / ray.d.y; } else { t_offset = (cloudTop - ray.o.y) / ray.d.y; } if (t_offset < 0) { return false; } // ray is entering cloud if (inCloud((ray.d.x * t_offset + ray.o.x) * inv_size + offsetX, (ray.d.z * t_offset + ray.o.z) * inv_size + offsetZ)) { ray.n.set(0, -Math.signum(ray.d.y), 0); onCloudEnter(ray, t_offset); return true; } } else if (inCloud(ray.o.x * inv_size + offsetX, ray.o.z * inv_size + offsetZ)) { target = 0; } double tExit; if (ray.d.y > 0) { tExit = (cloudTop - ray.o.y) / ray.d.y - t_offset; } else { tExit = (cloudBot - ray.o.y) / ray.d.y - t_offset; } if (ray.t < tExit) { tExit = ray.t; } double x0 = (ray.o.x + ray.d.x * t_offset) * inv_size + offsetX; double z0 = (ray.o.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); 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.setPrevMat(Block.AIR, 0); ray.setCurrentMat(Block.STONE, 0); // TODO add Cloud material } private static void onCloudExit(Ray ray, double t) { ray.t = t; ray.color.set(1, 1, 1, 1); ray.setPrevMat(Block.STONE, 0); ray.setCurrentMat(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; } }