diff --git a/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java b/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java index be240c673c..485e42fe2f 100644 --- a/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java +++ b/chunky/src/java/se/llbit/chunky/model/minecraft/CauldronModel.java @@ -20,6 +20,7 @@ import se.llbit.chunky.block.minecraft.Lava; import se.llbit.chunky.block.minecraft.Water; import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.renderer.scene.StillWaterShader; import se.llbit.chunky.resources.Texture; import se.llbit.math.Quad; import se.llbit.math.Ray; @@ -413,8 +414,8 @@ public static boolean intersectWithWater(Ray ray, Scene scene, int level) { // TODO since this water is the same block, refraction is not taken into account – still better than no water Quad water = waterLevels[level]; if (water != null && water.intersect(ray)) { - if (!scene.stillWaterEnabled()) { - scene.getWaterShading().doWaterShading(ray, scene.getAnimationTime()); + if (!(scene.getCurrentWaterShader() instanceof StillWaterShader)) { + scene.getCurrentWaterShader().doWaterShading(ray, scene.getAnimationTime()); } else { ray.setNormal(water.n); } diff --git a/chunky/src/java/se/llbit/chunky/renderer/WaterShadingStrategy.java b/chunky/src/java/se/llbit/chunky/renderer/WaterShadingStrategy.java new file mode 100644 index 0000000000..179d95dc4b --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/WaterShadingStrategy.java @@ -0,0 +1,48 @@ +/* Copyright (c) 2023 Chunky Contributors + * + * 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; + +import se.llbit.util.Registerable; + +public enum WaterShadingStrategy implements Registerable { + SIMPLEX("Simplex", "Uses configurable noise to shade the water, which prevents tiling at great distances."), + TILED_NORMALMAP("Tiled normal map", "Uses a built-in tiled normal map to shade the water"), + STILL("Still", "Renders the water surface as flat."); + + private final String displayName; + private final String description; + + WaterShadingStrategy(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + @Override + public String getName() { + return this.displayName; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String getId() { + return this.name(); + } +} diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/LegacyWaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/LegacyWaterShader.java index 83b4e42642..deff6a0ff5 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/LegacyWaterShader.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/LegacyWaterShader.java @@ -33,7 +33,6 @@ public WaterShader clone() { @Override public void save(JsonObject json) { - json.add("waterShader", "LEGACY"); } @Override 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 7e80122b2f..1e2127593a 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java @@ -93,10 +93,10 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int add Material currentMat = ray.getCurrentMaterial(); Material prevMat = ray.getPrevMaterial(); - if (!scene.stillWater && ray.getNormal().y != 0 && + if (!(scene.getCurrentWaterShader() instanceof StillWaterShader) && ray.getNormal().y != 0 && ((currentMat.isWater() && prevMat == Air.INSTANCE) || (currentMat == Air.INSTANCE && prevMat.isWater()))) { - scene.getWaterShading().doWaterShading(ray, scene.getAnimationTime()); + scene.getCurrentWaterShader().doWaterShading(ray, scene.getAnimationTime()); if (currentMat == Air.INSTANCE) { ray.invertNormal(); } 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 32b6b7340a..8720f5040b 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -221,14 +221,18 @@ public class Scene implements JsonSerializable, Refreshable { */ protected double waterOpacity = PersistentSettings.getWaterOpacity(); protected double waterVisibility = PersistentSettings.getWaterVisibility(); - protected boolean stillWater = PersistentSettings.getStillWater(); + protected WaterShadingStrategy waterShadingStrategy = WaterShadingStrategy.valueOf(PersistentSettings.getWaterShadingStrategy()); + private final StillWaterShader stillWaterShader = new StillWaterShader(); + private final LegacyWaterShader legacyWaterShader = new LegacyWaterShader(); + private final SimplexWaterShader simplexWaterShader = new SimplexWaterShader(); + + private WaterShader currentWaterShader = getWaterShader(waterShadingStrategy); protected boolean useCustomWaterColor = PersistentSettings.getUseCustomWaterColor(); protected boolean waterPlaneEnabled = false; protected double waterPlaneHeight = World.SEA_LEVEL; protected boolean waterPlaneOffsetEnabled = true; protected boolean waterPlaneChunkClip = true; - protected WaterShader waterShading = new SimplexWaterShader(); public final Fog fog = new Fog(this); @@ -440,7 +444,8 @@ public synchronized void copyState(Scene other, boolean copyChunks) { exposure = other.exposure; - stillWater = other.stillWater; + waterShadingStrategy = other.waterShadingStrategy; + currentWaterShader = other.currentWaterShader.clone(); waterOpacity = other.waterOpacity; waterVisibility = other.waterVisibility; useCustomWaterColor = other.useCustomWaterColor; @@ -466,7 +471,6 @@ public synchronized void copyState(Scene other, boolean copyChunks) { waterPlaneHeight = other.waterPlaneHeight; waterPlaneOffsetEnabled = other.waterPlaneOffsetEnabled; waterPlaneChunkClip = other.waterPlaneChunkClip; - waterShading = other.waterShading.clone(); hideUnknownBlocks = other.hideUnknownBlocks; @@ -612,16 +616,6 @@ public double getExposure() { return exposure; } - /** - * Set still water mode. - */ - public void setStillWater(boolean value) { - if (value != stillWater) { - stillWater = value; - refresh(); - } - } - /** * Set emitters enable flag. */ @@ -1617,13 +1611,6 @@ public synchronized void haltRender() { } } - /** - * @return true if still water is enabled - */ - public boolean stillWaterEnabled() { - return stillWater; - } - /** * @return true if biome colors are enabled */ @@ -2648,7 +2635,7 @@ public void setUseCustomWaterColor(boolean value) { json.add("fancierTranslucency", fancierTranslucency); json.add("transmissivityCap", transmissivityCap); json.add("sunSamplingStrategy", sunSamplingStrategy.getId()); - json.add("stillWater", stillWater); + json.add("waterShadingStrategy", waterShadingStrategy.getId()); json.add("waterOpacity", waterOpacity); json.add("waterVisibility", waterVisibility); json.add("useCustomWaterColor", useCustomWaterColor); @@ -2659,7 +2646,7 @@ public void setUseCustomWaterColor(boolean value) { colorObj.add("blue", waterColor.z); json.add("waterColor", colorObj); } - waterShading.save(json); + currentWaterShader.save(json); json.add("fog", fog.toJson()); json.add("biomeColorsEnabled", biomeColors); json.add("transparentSky", transparentSky); @@ -2930,7 +2917,28 @@ public synchronized void importFromJson(JsonObject json) { sunSamplingStrategy = SunSamplingStrategy.valueOf(json.get("sunSamplingStrategy").asString(SunSamplingStrategy.FAST.getId())); } - stillWater = json.get("stillWater").boolValue(stillWater); + waterShadingStrategy = WaterShadingStrategy.valueOf(json.get("waterShadingStrategy").asString(WaterShadingStrategy.SIMPLEX.getId())); + if (!json.get("waterShader").isUnknown()) { + String waterShader = json.get("waterShader").stringValue("SIMPLEX"); + if(waterShader.equals("LEGACY")) + waterShadingStrategy = WaterShadingStrategy.TILED_NORMALMAP; + else if(waterShader.equals("SIMPLEX")) + waterShadingStrategy = WaterShadingStrategy.SIMPLEX; + else { + Log.infof("Unknown water shader %s, using SIMPLEX", waterShader); + waterShadingStrategy = WaterShadingStrategy.SIMPLEX; + } + } else { + waterShadingStrategy = WaterShadingStrategy.TILED_NORMALMAP; + } + if (!json.get("stillWater").isUnknown()) { + if (json.get("stillWater").boolValue(false)) { + waterShadingStrategy = WaterShadingStrategy.STILL; + } + } + setCurrentWaterShader(waterShadingStrategy); + currentWaterShader.load(json); + waterOpacity = json.get("waterOpacity").doubleValue(waterOpacity); waterVisibility = json.get("waterVisibility").doubleValue(waterVisibility); useCustomWaterColor = json.get("useCustomWaterColor").boolValue(useCustomWaterColor); @@ -2940,16 +2948,6 @@ public synchronized void importFromJson(JsonObject json) { waterColor.y = colorObj.get("green").doubleValue(waterColor.y); waterColor.z = colorObj.get("blue").doubleValue(waterColor.z); } - String waterShader = json.get("waterShader").stringValue("SIMPLEX"); - if(waterShader.equals("LEGACY")) - waterShading = new LegacyWaterShader(); - else if(waterShader.equals("SIMPLEX")) - waterShading = new SimplexWaterShader(); - else { - Log.infof("Unknown water shader %s, using SIMPLEX", waterShader); - waterShading = new SimplexWaterShader(); - } - waterShading.load(json); biomeColors = json.get("biomeColorsEnabled").boolValue(biomeColors); transparentSky = json.get("transparentSky").boolValue(transparentSky); JsonValue fogObj = json.get("fog"); @@ -3393,18 +3391,32 @@ public World getWorld() { return loadedWorld; } - /** - * Get the water shader - */ - public WaterShader getWaterShading() { - return waterShading; + public WaterShadingStrategy getWaterShadingStrategy() { + return waterShadingStrategy; } - /** - * Set the Water shader - */ - public void setWaterShading(WaterShader waterShading) { - this.waterShading = waterShading; + public void setWaterShadingStrategy(WaterShadingStrategy waterShadingStrategy) { + this.waterShadingStrategy = waterShadingStrategy; + setCurrentWaterShader(waterShadingStrategy); + refresh(); + } + + public WaterShader getCurrentWaterShader() { + return currentWaterShader; + } + private void setCurrentWaterShader(WaterShadingStrategy waterShadingStrategy) { + currentWaterShader = getWaterShader(waterShadingStrategy); + } + + private WaterShader getWaterShader(WaterShadingStrategy waterShadingStrategy) { + switch (waterShadingStrategy) { + case STILL: + return stillWaterShader; + case TILED_NORMALMAP: + return legacyWaterShader; + default: + return simplexWaterShader; + } } public boolean getHideUnknownBlocks() { diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexWaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexWaterShader.java index b3dbd62deb..682a231900 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexWaterShader.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/SimplexWaterShader.java @@ -79,7 +79,6 @@ public WaterShader clone() { @Override public void save(JsonObject json) { - json.add("waterShader", "SIMPLEX"); JsonObject params = new JsonObject(); params.add("iterations", iterations); params.add("frequency", baseFrequency); diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/StillWaterShader.java b/chunky/src/java/se/llbit/chunky/renderer/scene/StillWaterShader.java new file mode 100644 index 0000000000..1e3347d6ba --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/StillWaterShader.java @@ -0,0 +1,40 @@ +/* Copyright (c) 2012-2023 Chunky contributors + * + * 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.scene; + +import se.llbit.chunky.model.minecraft.WaterModel; +import se.llbit.json.JsonObject; +import se.llbit.math.Ray; + +public class StillWaterShader implements WaterShader { + @Override + public void doWaterShading(Ray ray, double animationTime) { + } + + @Override + public WaterShader clone() { + return new StillWaterShader(); + } + + @Override + public void save(JsonObject json) { + } + + @Override + public void load(JsonObject json) { + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java index 34f3a440b9..6efb2a7653 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/WaterTab.java @@ -26,10 +26,8 @@ import javafx.scene.paint.Color; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.renderer.RenderController; -import se.llbit.chunky.renderer.scene.LegacyWaterShader; -import se.llbit.chunky.renderer.scene.Scene; -import se.llbit.chunky.renderer.scene.SimplexWaterShader; -import se.llbit.chunky.renderer.scene.WaterShader; +import se.llbit.chunky.renderer.WaterShadingStrategy; +import se.llbit.chunky.renderer.scene.*; import se.llbit.chunky.ui.DoubleAdjuster; import se.llbit.chunky.ui.IntegerAdjuster; import se.llbit.chunky.ui.controller.RenderControlsFxController; @@ -46,7 +44,7 @@ public class WaterTab extends ScrollPane implements RenderControlsTab, Initializable { private Scene scene; - @FXML private CheckBox stillWater; + @FXML private ChoiceBox waterShader; @FXML private DoubleAdjuster waterVisibility; @FXML private DoubleAdjuster waterOpacity; @FXML private CheckBox useCustomWaterColor; @@ -57,7 +55,6 @@ public class WaterTab extends ScrollPane implements RenderControlsTab, Initializ @FXML private CheckBox waterPlaneOffsetEnabled; @FXML private CheckBox waterPlaneClip; @FXML private TitledPane waterWorldModeDetailsPane; - @FXML private CheckBox useProceduralWater; @FXML private IntegerAdjuster proceduralWaterIterations; @FXML private DoubleAdjuster proceduralWaterFrequency; @FXML private DoubleAdjuster proceduralWaterAmplitude; @@ -89,7 +86,7 @@ public void setController(RenderControlsFxController controller) { @Override public void update(Scene scene) { useCustomWaterColor.setSelected(scene.getUseCustomWaterColor()); - stillWater.setSelected(scene.stillWaterEnabled()); + waterShader.getSelectionModel().select(scene.getWaterShadingStrategy()); waterVisibility.set(scene.getWaterVisibility()); waterOpacity.set(scene.getWaterOpacity()); @@ -104,15 +101,13 @@ public void update(Scene scene) { waterPlaneOffsetEnabled.setSelected(scene.isWaterPlaneOffsetEnabled()); waterPlaneClip.setSelected(scene.getWaterPlaneChunkClip()); - if(scene.getWaterShading() instanceof SimplexWaterShader) { - useProceduralWater.setSelected(true); - SimplexWaterShader simplexWaterShader = (SimplexWaterShader) scene.getWaterShading(); + if(scene.getCurrentWaterShader() instanceof SimplexWaterShader) { + SimplexWaterShader simplexWaterShader = (SimplexWaterShader) scene.getCurrentWaterShader(); proceduralWaterIterations.set(simplexWaterShader.iterations); proceduralWaterFrequency.set(simplexWaterShader.baseFrequency); proceduralWaterAmplitude.set(simplexWaterShader.baseAmplitude); proceduralWaterAnimationSpeed.set(simplexWaterShader.animationSpeed); } else { - useProceduralWater.setSelected(false); proceduralWaterIterations.set(4); proceduralWaterFrequency.set(0.4); proceduralWaterAmplitude.set(0.025); @@ -144,10 +139,28 @@ public void initialize(URL location, ResourceBundle resources) { waterOpacity.clampBoth(); waterOpacity.onValueChange(value -> scene.setWaterOpacity(value)); - stillWater.setTooltip(new Tooltip("Disable the waves on the water surface.")); - stillWater.selectedProperty().addListener((observable, oldValue, newValue) -> - scene.setStillWater(newValue) - ); + waterShader.getItems().addAll(WaterShadingStrategy.values()); + waterShader.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + scene.setWaterShadingStrategy(newValue); + switch (newValue) { + case STILL: + case TILED_NORMALMAP: + proceduralWaterDetailsPane.setVisible(false); + proceduralWaterDetailsPane.setExpanded(false); + proceduralWaterDetailsPane.setManaged(false); + break; + case SIMPLEX: + proceduralWaterDetailsPane.setVisible(true); + proceduralWaterDetailsPane.setExpanded(true); + proceduralWaterDetailsPane.setManaged(true); + break; + } + }); + StringBuilder waterShaderOptions = new StringBuilder("\n\n"); + for (WaterShadingStrategy strategy : WaterShadingStrategy.values()) { + waterShaderOptions.append(strategy.getId()).append(": ").append(strategy.getDescription()).append("\n"); + } + waterShader.setTooltip(new Tooltip("Change how the water surface is rendered." + waterShaderOptions)); useCustomWaterColor.setTooltip(new Tooltip("Disable biome tinting for water, and use a custom color instead.")); useCustomWaterColor.selectedProperty().addListener((observable, oldValue, newValue) -> @@ -158,7 +171,7 @@ public void initialize(URL location, ResourceBundle resources) { saveDefaults.setTooltip(new Tooltip("Save the current water settings as new defaults.")); saveDefaults.setOnAction(e -> { - PersistentSettings.setStillWater(scene.stillWaterEnabled()); + PersistentSettings.setWaterShadingStrategy(scene.getWaterShadingStrategy().getId()); PersistentSettings.setWaterOpacity(scene.getWaterOpacity()); PersistentSettings.setWaterVisibility(scene.getWaterVisibility()); boolean useCustomWaterColor = scene.getUseCustomWaterColor(); @@ -195,34 +208,11 @@ public void initialize(URL location, ResourceBundle resources) { scene.setWaterPlaneChunkClip(newValue) ); - proceduralWaterDetailsPane.setVisible(useProceduralWater.isSelected()); - proceduralWaterDetailsPane.setExpanded(useProceduralWater.isSelected()); - proceduralWaterDetailsPane.setManaged(useProceduralWater.isSelected()); - - useProceduralWater.setTooltip(new Tooltip("Generate customized water waves using noise to prevent tiling at large distances.")); - useProceduralWater.selectedProperty().addListener((observable, oldValue, newValue) -> { - if(newValue && scene.getWaterShading() instanceof LegacyWaterShader) { - SimplexWaterShader shader = new SimplexWaterShader(); - scene.setWaterShading(shader); - shader.iterations = proceduralWaterIterations.get(); - shader.baseFrequency = proceduralWaterFrequency.get(); - shader.baseAmplitude = proceduralWaterAmplitude.get(); - shader.animationSpeed = proceduralWaterAnimationSpeed.get(); - scene.refresh(); - } else if(!newValue && scene.getWaterShading() instanceof SimplexWaterShader) { - scene.setWaterShading(new LegacyWaterShader()); - scene.refresh(); - } - proceduralWaterDetailsPane.setVisible(newValue); - proceduralWaterDetailsPane.setExpanded(newValue); - proceduralWaterDetailsPane.setManaged(newValue); - }); - proceduralWaterIterations.setName("Iterations"); proceduralWaterIterations.setTooltip("The number of iterations (layers) of noise used"); proceduralWaterIterations.setRange(1, 10); proceduralWaterIterations.onValueChange(iter -> { - WaterShader shader = scene.getWaterShading(); + WaterShader shader = scene.getCurrentWaterShader(); if(shader instanceof SimplexWaterShader) { ((SimplexWaterShader) shader).iterations = iter; scene.refresh(); @@ -233,7 +223,7 @@ public void initialize(URL location, ResourceBundle resources) { proceduralWaterFrequency.setTooltip("The frequency of the noise"); proceduralWaterFrequency.setRange(0, 1); proceduralWaterFrequency.onValueChange(freq -> { - WaterShader shader = scene.getWaterShading(); + WaterShader shader = scene.getCurrentWaterShader(); if(shader instanceof SimplexWaterShader) { ((SimplexWaterShader) shader).baseFrequency = freq; } @@ -244,7 +234,7 @@ public void initialize(URL location, ResourceBundle resources) { proceduralWaterAmplitude.setTooltip("The amplitude of the noise"); proceduralWaterAmplitude.setRange(0, 1); proceduralWaterAmplitude.onValueChange(amp -> { - WaterShader shader = scene.getWaterShading(); + WaterShader shader = scene.getCurrentWaterShader(); if(shader instanceof SimplexWaterShader) { ((SimplexWaterShader) shader).baseAmplitude = amp; } @@ -256,7 +246,7 @@ public void initialize(URL location, ResourceBundle resources) { + " Only relevant when rendering animation by varying animation time."); proceduralWaterAnimationSpeed.setRange(0, 10); proceduralWaterAnimationSpeed.onValueChange(speed -> { - WaterShader shader = scene.getWaterShading(); + WaterShader shader = scene.getCurrentWaterShader(); if(shader instanceof SimplexWaterShader) { ((SimplexWaterShader) shader).animationSpeed = speed; } diff --git a/chunky/src/res/se/llbit/chunky/ui/render/tabs/WaterTab.fxml b/chunky/src/res/se/llbit/chunky/ui/render/tabs/WaterTab.fxml index 39ff6f2d90..77794b11a6 100644 --- a/chunky/src/res/se/llbit/chunky/ui/render/tabs/WaterTab.fxml +++ b/chunky/src/res/se/llbit/chunky/ui/render/tabs/WaterTab.fxml @@ -3,18 +3,35 @@ + + - - + + + - + + + + + + + + + + + + + @@ -34,18 +51,6 @@ - - - - - - - - - - - - diff --git a/lib/src/se/llbit/chunky/PersistentSettings.java b/lib/src/se/llbit/chunky/PersistentSettings.java index 1f38e64a0f..c72c11e53c 100644 --- a/lib/src/se/llbit/chunky/PersistentSettings.java +++ b/lib/src/se/llbit/chunky/PersistentSettings.java @@ -356,13 +356,18 @@ public static void setFollowCamera(boolean value) { save(); } - public static void setStillWater(boolean value) { - settings.setBool("stillWater", value); + public static boolean getStillWater() { + return settings.getBool("stillWater", false); + } + + public static void setWaterShadingStrategy(String waterShadingStrategy) { + settings.setString("waterShadingStrategy", waterShadingStrategy); save(); } - public static boolean getStillWater() { - return settings.getBool("stillWater", false); + public static String getWaterShadingStrategy() { + String defaultValue = getStillWater() ? "STILL" : "SIMPLEX"; + return settings.getString("waterShadingStrategy", defaultValue); } public static void setWaterOpacity(double value) {