diff --git a/src/main/java/net/coderbot/iris/colorspace/ColorSpace.java b/src/main/java/net/coderbot/iris/colorspace/ColorSpace.java new file mode 100644 index 0000000000..957d165531 --- /dev/null +++ b/src/main/java/net/coderbot/iris/colorspace/ColorSpace.java @@ -0,0 +1,9 @@ +package net.coderbot.iris.colorspace; + +public enum ColorSpace { + SRGB, + DCI_P3, + DISPLAY_P3, + REC2020, + ADOBE_RGB +} \ No newline at end of file diff --git a/src/main/java/net/coderbot/iris/colorspace/ColorSpaceComputeConverter.java b/src/main/java/net/coderbot/iris/colorspace/ColorSpaceComputeConverter.java new file mode 100644 index 0000000000..a653f77a39 --- /dev/null +++ b/src/main/java/net/coderbot/iris/colorspace/ColorSpaceComputeConverter.java @@ -0,0 +1,70 @@ +package net.coderbot.iris.colorspace; + +import com.google.common.collect.ImmutableSet; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.program.ComputeProgram; +import net.coderbot.iris.gl.program.ProgramBuilder; +import net.coderbot.iris.gl.texture.InternalTextureFormat; +import net.coderbot.iris.shaderpack.StringPair; +import net.coderbot.iris.shaderpack.preprocessor.JcppProcessor; +import org.apache.commons.io.IOUtils; +import org.lwjgl.opengl.GL43C; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class ColorSpaceComputeConverter implements ColorSpaceConverter { + private int width; + private int height; + private ColorSpace colorSpace; + private ComputeProgram program; + + private int target; + public ColorSpaceComputeConverter(int width, int height, ColorSpace colorSpace) { + rebuildProgram(width, height, colorSpace); + } + + public void rebuildProgram(int width, int height, ColorSpace colorSpace) { + if (program != null) { + program.destroy(); + program = null; + } + + this.width = width; + this.height = height; + this.colorSpace = colorSpace; + + String source; + try { + source = new String(IOUtils.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream("/colorSpace.csh"))), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + + List defineList = new ArrayList<>(); + defineList.add(new StringPair("COMPUTE", "")); + defineList.add(new StringPair("CURRENT_COLOR_SPACE", String.valueOf(colorSpace.ordinal()))); + + for (ColorSpace space : ColorSpace.values()) { + defineList.add(new StringPair(space.name(), String.valueOf(space.ordinal()))); + } + source = JcppProcessor.glslPreprocessSource(source, defineList); + ProgramBuilder builder = ProgramBuilder.beginCompute("colorSpaceCompute", source, ImmutableSet.of()); + builder.addTextureImage(() -> target, InternalTextureFormat.RGBA8, "readImage"); + this.program = builder.buildCompute(); + } + + public void process(int targetImage) { + if (colorSpace == ColorSpace.SRGB) return; + + this.target = targetImage; + program.use(); + IrisRenderSystem.dispatchCompute(width / 8, height / 8, 1); + IrisRenderSystem.memoryBarrier(GL43C.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL43C.GL_TEXTURE_FETCH_BARRIER_BIT); + ComputeProgram.unbind(); + } +} \ No newline at end of file diff --git a/src/main/java/net/coderbot/iris/colorspace/ColorSpaceConverter.java b/src/main/java/net/coderbot/iris/colorspace/ColorSpaceConverter.java new file mode 100644 index 0000000000..1d444defd7 --- /dev/null +++ b/src/main/java/net/coderbot/iris/colorspace/ColorSpaceConverter.java @@ -0,0 +1,6 @@ +package net.coderbot.iris.colorspace; + +public interface ColorSpaceConverter { + void rebuildProgram(int width, int height, ColorSpace colorSpace); + void process(int target); +} \ No newline at end of file diff --git a/src/main/java/net/coderbot/iris/colorspace/ColorSpaceFragmentConverter.java b/src/main/java/net/coderbot/iris/colorspace/ColorSpaceFragmentConverter.java new file mode 100644 index 0000000000..2fd4640e61 --- /dev/null +++ b/src/main/java/net/coderbot/iris/colorspace/ColorSpaceFragmentConverter.java @@ -0,0 +1,90 @@ +package net.coderbot.iris.colorspace; + +import com.google.common.collect.ImmutableSet; +import com.mojang.blaze3d.platform.GlStateManager; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gl.framebuffer.GlFramebuffer; +import net.coderbot.iris.gl.program.Program; +import net.coderbot.iris.gl.program.ProgramBuilder; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.coderbot.iris.postprocess.FullScreenQuadRenderer; +import net.coderbot.iris.shaderpack.StringPair; +import net.coderbot.iris.shaderpack.preprocessor.JcppProcessor; +import net.coderbot.iris.vendored.joml.Matrix4f; +import org.apache.commons.io.IOUtils; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL30C; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class ColorSpaceFragmentConverter implements ColorSpaceConverter { + private int width; + private int height; + private ColorSpace colorSpace; + private Program program; + private GlFramebuffer framebuffer; + private int swapTexture; + + private int target; + public ColorSpaceFragmentConverter(int width, int height, ColorSpace colorSpace) { + rebuildProgram(width, height, colorSpace); + } + + public void rebuildProgram(int width, int height, ColorSpace colorSpace) { + if (program != null) { + program.destroy(); + program = null; + framebuffer.destroy(); + framebuffer = null; + GlStateManager._deleteTexture(swapTexture); + swapTexture = 0; + } + + this.width = width; + this.height = height; + this.colorSpace = colorSpace; + + String vertexSource; + String source; + try { + vertexSource = new String(IOUtils.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream("/colorSpace.vsh"))), StandardCharsets.UTF_8); + source = new String(IOUtils.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream("/colorSpace.csh"))), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + + List defineList = new ArrayList<>(); + defineList.add(new StringPair("CURRENT_COLOR_SPACE", String.valueOf(colorSpace.ordinal()))); + + for (ColorSpace space : ColorSpace.values()) { + defineList.add(new StringPair(space.name(), String.valueOf(space.ordinal()))); + } + source = JcppProcessor.glslPreprocessSource(source, defineList); + + ProgramBuilder builder = ProgramBuilder.begin("colorSpaceFragment", vertexSource, null, source, ImmutableSet.of()); + + builder.uniformJomlMatrix(UniformUpdateFrequency.ONCE, "projection", () -> new Matrix4f(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, -1, -1, 0, 1)); + builder.addDynamicSampler(() -> target, "readImage"); + + swapTexture = GlStateManager._genTexture(); + IrisRenderSystem.texImage2D(swapTexture, GL30C.GL_TEXTURE_2D, 0, GL30C.GL_RGBA8, width, height, 0, GL30C.GL_RGBA, GL30C.GL_UNSIGNED_BYTE, null); + + this.framebuffer = new GlFramebuffer(); + framebuffer.addColorAttachment(0, swapTexture); + this.program = builder.build(); + } + + public void process(int targetImage) { + if (colorSpace == ColorSpace.SRGB) return; + + this.target = targetImage; + program.use(); + framebuffer.bind(); + FullScreenQuadRenderer.INSTANCE.render(); + Program.unbind(); + framebuffer.bindAsReadBuffer(); + IrisRenderSystem.copyTexSubImage2D(targetImage, GL11C.GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height); + } +} \ No newline at end of file diff --git a/src/main/java/net/coderbot/iris/compat/sodium/impl/options/IrisSodiumOptions.java b/src/main/java/net/coderbot/iris/compat/sodium/impl/options/IrisSodiumOptions.java index 69a3f9114b..6888b656f4 100644 --- a/src/main/java/net/coderbot/iris/compat/sodium/impl/options/IrisSodiumOptions.java +++ b/src/main/java/net/coderbot/iris/compat/sodium/impl/options/IrisSodiumOptions.java @@ -10,9 +10,11 @@ import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl; import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; import net.coderbot.iris.Iris; +import net.coderbot.iris.colorspace.ColorSpace; import net.coderbot.iris.gui.option.IrisVideoSettings; import net.minecraft.client.Options; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; import net.minecraft.network.chat.TranslatableComponent; public class IrisSodiumOptions { @@ -39,6 +41,29 @@ public static OptionImpl createMaxShadowDistanceSlider(Minecra return maxShadowDistanceSlider; } + public static OptionImpl createColorSpaceButton(MinecraftOptionsStorage vanillaOpts) { + OptionImpl colorSpace = OptionImpl.createBuilder(ColorSpace.class, vanillaOpts) + .setName(new TranslatableComponent("options.iris.colorSpace")) + .setTooltip(new TranslatableComponent("options.iris.colorSpace.sodium_tooltip")) + .setControl(option -> new CyclingControl<>(option, ColorSpace.class, + new Component[] { new TextComponent("SRGB"), new TextComponent("DCI_P3"), new TextComponent("Display P3"), new TextComponent("REC2020"), new TextComponent("Adobe RGB") })) + .setBinding((options, value) -> { + IrisVideoSettings.colorSpace = value; + try { + Iris.getIrisConfig().save(); + } catch (IOException e) { + e.printStackTrace(); + } + }, + options -> IrisVideoSettings.colorSpace) + .setImpact(OptionImpact.LOW) + .setEnabled(true) + .build(); + + + return colorSpace; + } + public static OptionImpl createLimitedVideoSettingsButton(MinecraftOptionsStorage vanillaOpts) { return OptionImpl.createBuilder(SupportedGraphicsMode.class, vanillaOpts) .setName(new TranslatableComponent("options.graphics")) diff --git a/src/main/java/net/coderbot/iris/compat/sodium/mixin/options/MixinSodiumGameOptionPages.java b/src/main/java/net/coderbot/iris/compat/sodium/mixin/options/MixinSodiumGameOptionPages.java index 6099e1823e..f3b6b16f14 100644 --- a/src/main/java/net/coderbot/iris/compat/sodium/mixin/options/MixinSodiumGameOptionPages.java +++ b/src/main/java/net/coderbot/iris/compat/sodium/mixin/options/MixinSodiumGameOptionPages.java @@ -40,6 +40,7 @@ public class MixinSodiumGameOptionPages { Option candidate) { builder.add(candidate); builder.add(IrisSodiumOptions.createMaxShadowDistanceSlider(vanillaOpts)); + builder.add(IrisSodiumOptions.createColorSpaceButton(vanillaOpts)); return builder; } diff --git a/src/main/java/net/coderbot/iris/config/IrisConfig.java b/src/main/java/net/coderbot/iris/config/IrisConfig.java index e15f76db5b..0070f570cd 100644 --- a/src/main/java/net/coderbot/iris/config/IrisConfig.java +++ b/src/main/java/net/coderbot/iris/config/IrisConfig.java @@ -9,6 +9,7 @@ import java.util.Properties; import net.coderbot.iris.Iris; +import net.coderbot.iris.colorspace.ColorSpace; import net.coderbot.iris.gui.option.IrisVideoSettings; /** @@ -139,9 +140,11 @@ public void load() throws IOException { disableUpdateMessage = "true".equals(properties.getProperty("disableUpdateMessage")); try { IrisVideoSettings.shadowDistance = Integer.parseInt(properties.getProperty("maxShadowRenderDistance", "32")); - } catch (NumberFormatException e) { + IrisVideoSettings.colorSpace = ColorSpace.valueOf(properties.getProperty("colorSpace", "SRGB")); + } catch (IllegalArgumentException e) { Iris.logger.error("Shadow distance setting reset; value is invalid."); IrisVideoSettings.shadowDistance = 32; + IrisVideoSettings.colorSpace = ColorSpace.SRGB; save(); } @@ -164,6 +167,7 @@ public void save() throws IOException { properties.setProperty("enableDebugOptions", enableDebugOptions ? "true" : "false"); properties.setProperty("disableUpdateMessage", disableUpdateMessage ? "true" : "false"); properties.setProperty("maxShadowRenderDistance", String.valueOf(IrisVideoSettings.shadowDistance)); + properties.setProperty("colorSpace", IrisVideoSettings.colorSpace.name()); // NB: This uses ISO-8859-1 with unicode escapes as the encoding try (OutputStream os = Files.newOutputStream(propertiesPath)) { properties.store(os, COMMENT); diff --git a/src/main/java/net/coderbot/iris/gl/program/ComputeProgram.java b/src/main/java/net/coderbot/iris/gl/program/ComputeProgram.java index 25d6c09e7b..ef68c0de9b 100644 --- a/src/main/java/net/coderbot/iris/gl/program/ComputeProgram.java +++ b/src/main/java/net/coderbot/iris/gl/program/ComputeProgram.java @@ -56,6 +56,14 @@ public Vector3i getWorkGroups(float width, float height) { return cachedWorkGroups; } + public void use() { + ProgramManager.glUseProgram(getGlId()); + + uniforms.update(); + samplers.update(); + images.update(); + } + public void dispatch(float width, float height) { ProgramManager.glUseProgram(getGlId()); uniforms.update(); diff --git a/src/main/java/net/coderbot/iris/gui/option/IrisVideoSettings.java b/src/main/java/net/coderbot/iris/gui/option/IrisVideoSettings.java index 1510ab5ea3..047386a0be 100644 --- a/src/main/java/net/coderbot/iris/gui/option/IrisVideoSettings.java +++ b/src/main/java/net/coderbot/iris/gui/option/IrisVideoSettings.java @@ -3,6 +3,7 @@ import java.io.IOException; import net.coderbot.iris.Iris; +import net.coderbot.iris.colorspace.ColorSpace; import net.coderbot.iris.pipeline.WorldRenderingPipeline; import net.minecraft.client.Minecraft; import net.minecraft.client.ProgressOption; @@ -15,6 +16,7 @@ public class IrisVideoSettings { // TODO: Tell the user to check in the shader options once that's supported. private static final Component DISABLED_TOOLTIP = new TranslatableComponent("options.iris.shadowDistance.disabled"); private static final Component ENABLED_TOOLTIP = new TranslatableComponent("options.iris.shadowDistance.enabled"); + public static ColorSpace colorSpace = ColorSpace.SRGB; public static int getOverriddenShadowDistance(int base) { return Iris.getPipelineManager().getPipeline() diff --git a/src/main/java/net/coderbot/iris/mixin/MixinDebugScreenOverlay.java b/src/main/java/net/coderbot/iris/mixin/MixinDebugScreenOverlay.java index cf77a644e5..37f63d0a1b 100644 --- a/src/main/java/net/coderbot/iris/mixin/MixinDebugScreenOverlay.java +++ b/src/main/java/net/coderbot/iris/mixin/MixinDebugScreenOverlay.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; +import net.coderbot.iris.gui.option.IrisVideoSettings; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -51,6 +52,7 @@ public abstract class MixinDebugScreenOverlay { Iris.getCurrentPack().ifPresent(pack -> { messages.add("[" + Iris.MODNAME + "] " + pack.getProfileInfo()); }); + messages.add("[" + Iris.MODNAME + "] Color space: " + IrisVideoSettings.colorSpace.name()); } else { messages.add("[" + Iris.MODNAME + "] Shaders are disabled"); } diff --git a/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java index e33df1a654..67fe37d6b0 100644 --- a/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java +++ b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java @@ -12,6 +12,12 @@ import java.util.function.IntFunction; import java.util.function.Supplier; +import net.coderbot.iris.colorspace.ColorSpace; +import net.coderbot.iris.colorspace.ColorSpaceComputeConverter; +import net.coderbot.iris.colorspace.ColorSpaceConverter; +import net.coderbot.iris.colorspace.ColorSpaceFragmentConverter; +import net.coderbot.iris.gl.IrisRenderSystem; +import net.coderbot.iris.gui.option.IrisVideoSettings; import org.jetbrains.annotations.Nullable; import org.lwjgl.opengl.GL15C; import org.lwjgl.opengl.GL20C; @@ -126,6 +132,8 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R private final SodiumTerrainPipeline sodiumTerrainPipeline; + private final ColorSpaceConverter colorSpaceConverter; + private final HorizonRenderer horizonRenderer = new HorizonRenderer(); private final float sunPathRotation; @@ -154,6 +162,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R private int currentNormalTexture; private int currentSpecularTexture; private PackDirectives packDirectives; + private ColorSpace currentColorSpace; public DeferredWorldRenderingPipeline(ProgramSet programs) { Objects.requireNonNull(programs); @@ -430,6 +439,28 @@ public DeferredWorldRenderingPipeline(ProgramSet programs) { this.sodiumTerrainPipeline = new SodiumTerrainPipeline(this, programs, createTerrainSamplers, shadowRenderer == null ? null : createShadowTerrainSamplers, createTerrainImages, shadowRenderer == null ? null : createShadowTerrainImages); + + if (programs.getPackDirectives().supportsColorCorrection()) { + colorSpaceConverter = new ColorSpaceConverter() { + @Override + public void rebuildProgram(int width, int height, ColorSpace colorSpace) { + + } + + @Override + public void process(int target) { + + } + }; + } else { + if (IrisRenderSystem.supportsCompute()) { + colorSpaceConverter = new ColorSpaceComputeConverter(mainTarget.width, mainTarget.height, IrisVideoSettings.colorSpace); + } else { + colorSpaceConverter = new ColorSpaceFragmentConverter(mainTarget.width, mainTarget.height, IrisVideoSettings.colorSpace); + } + } + + currentColorSpace = IrisVideoSettings.colorSpace; } private RenderTargets getRenderTargets() { @@ -939,6 +970,11 @@ private void prepareRenderTargets() { packDirectives.getRenderTargetDirectives()); } + if (changed || IrisVideoSettings.colorSpace != currentColorSpace) { + currentColorSpace = IrisVideoSettings.colorSpace; + colorSpaceConverter.rebuildProgram(main.width, main.height, currentColorSpace); + } + final ImmutableList passes; if (renderTargets.isFullClearRequired()) { @@ -1171,6 +1207,7 @@ public void finalizeLevelRendering() { compositeRenderer.renderAll(); finalPassRenderer.renderFinalPass(); + colorSpaceConverter.process(Minecraft.getInstance().getMainRenderTarget().getColorTextureId()); isRenderingFullScreenPass = false; } diff --git a/src/main/java/net/coderbot/iris/shaderpack/PackDirectives.java b/src/main/java/net/coderbot/iris/shaderpack/PackDirectives.java index 2ed3595a62..39bf486330 100644 --- a/src/main/java/net/coderbot/iris/shaderpack/PackDirectives.java +++ b/src/main/java/net/coderbot/iris/shaderpack/PackDirectives.java @@ -13,6 +13,7 @@ import net.coderbot.iris.vendored.joml.Vector2i; public class PackDirectives { + private boolean supportsColorCorrection; private int noiseTextureResolution; private float sunPathRotation; private float ambientOcclusionLevel; @@ -41,6 +42,7 @@ public class PackDirectives { private PackDirectives(Set supportedRenderTargets, PackShadowDirectives packShadowDirectives) { noiseTextureResolution = 256; sunPathRotation = 0.0F; + supportsColorCorrection = false; ambientOcclusionLevel = 1.0F; wetnessHalfLife = 600.0f; drynessHalfLife = 200.0f; @@ -60,6 +62,7 @@ private PackDirectives(Set supportedRenderTargets, PackShadowDirectives rainDepth = properties.getRainDepth().orElse(false); separateAo = properties.getSeparateAo().orElse(false); oldLighting = properties.getOldLighting().orElse(false); + supportsColorCorrection = properties.supportsColorCorrection().orElse(false); concurrentCompute = properties.getConcurrentCompute().orElse(false); oldHandLight = properties.getOldHandLight().orElse(true); explicitFlips = properties.getExplicitFlips(); @@ -164,6 +167,10 @@ public PackShadowDirectives getShadowDirectives() { return shadowDirectives; } + public boolean supportsColorCorrection() { + return supportsColorCorrection; + } + private static float clamp(float val, float lo, float hi) { return Math.max(lo, Math.min(hi, val)); } diff --git a/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java b/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java index 77cc6d818b..e720287837 100644 --- a/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java +++ b/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import net.coderbot.iris.colorspace.ColorSpace; import net.coderbot.iris.shaderpack.materialmap.NamespacedId; import net.coderbot.iris.shaderpack.preprocessor.PropertiesPreprocessor; import org.jetbrains.annotations.Nullable; @@ -158,16 +159,23 @@ public ShaderPack(Path root, Map changedConfigs, Iterable newEnvDefines = new ArrayList<>(); + environmentDefines.forEach(newEnvDefines::add); + + if (shaderProperties.supportsColorCorrection().orElse(false)) { + for (ColorSpace space : ColorSpace.values()) { + newEnvDefines.add(new StringPair("COLOR_SPACE_" + space.name(), String.valueOf(space.ordinal()))); + } + } List optionalFeatureFlags = shaderProperties.getOptionalFeatureFlags().stream().filter(flag -> !FeatureFlags.isInvalid(flag)).collect(Collectors.toList()); if (!optionalFeatureFlags.isEmpty()) { - List newEnvDefines = new ArrayList<>(); - environmentDefines.forEach(newEnvDefines::add); optionalFeatureFlags.forEach(flag -> newEnvDefines.add(new StringPair("IRIS_FEATURE_" + flag, ""))); - environmentDefines = ImmutableList.copyOf(newEnvDefines); } + environmentDefines = ImmutableList.copyOf(newEnvDefines); + ProfileSet profiles = ProfileSet.fromTree(shaderProperties.getProfiles(), this.shaderPackOptions.getOptionSet()); this.profile = profiles.scan(this.shaderPackOptions.getOptionSet(), this.shaderPackOptions.getOptionValues()); diff --git a/src/main/java/net/coderbot/iris/shaderpack/ShaderProperties.java b/src/main/java/net/coderbot/iris/shaderpack/ShaderProperties.java index 63eb5fed15..3a55acab2e 100644 --- a/src/main/java/net/coderbot/iris/shaderpack/ShaderProperties.java +++ b/src/main/java/net/coderbot/iris/shaderpack/ShaderProperties.java @@ -44,6 +44,7 @@ public class ShaderProperties { private CloudSetting cloudSetting = CloudSetting.DEFAULT; private OptionalBoolean oldHandLight = OptionalBoolean.DEFAULT; private OptionalBoolean dynamicHandLight = OptionalBoolean.DEFAULT; + private OptionalBoolean supportsColorCorrection = OptionalBoolean.DEFAULT; private OptionalBoolean oldLighting = OptionalBoolean.DEFAULT; private OptionalBoolean shadowTerrain = OptionalBoolean.DEFAULT; private OptionalBoolean shadowTranslucent = OptionalBoolean.DEFAULT; @@ -149,6 +150,7 @@ public ShaderProperties(String contents, ShaderPackOptions shaderPackOptions, It handleBooleanDirective(key, value, "shadow.culling", bool -> shadowCulling = bool); handleBooleanDirective(key, value, "particles.before.deferred", bool -> particlesBeforeDeferred = bool); handleBooleanDirective(key, value, "prepareBeforeShadow", bool -> prepareBeforeShadow = bool); + handleBooleanDirective(key, value, "supportsColorCorrection", bool -> supportsColorCorrection = bool); // TODO: Min optifine versions, shader options layout / appearance / profiles // TODO: Custom uniforms @@ -620,4 +622,8 @@ public List getRequiredFeatureFlags() { public List getOptionalFeatureFlags() { return optionalFeatureFlags; } + + public OptionalBoolean supportsColorCorrection() { + return supportsColorCorrection; + } } diff --git a/src/main/java/net/coderbot/iris/uniforms/IrisExclusiveUniforms.java b/src/main/java/net/coderbot/iris/uniforms/IrisExclusiveUniforms.java index 3fefeba364..30effdca04 100644 --- a/src/main/java/net/coderbot/iris/uniforms/IrisExclusiveUniforms.java +++ b/src/main/java/net/coderbot/iris/uniforms/IrisExclusiveUniforms.java @@ -6,6 +6,7 @@ import net.coderbot.iris.JomlConversions; import net.coderbot.iris.gl.uniform.UniformHolder; import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.coderbot.iris.gui.option.IrisVideoSettings; import net.coderbot.iris.mixin.DimensionTypeAccessor; import net.coderbot.iris.vendored.joml.Math; import net.coderbot.iris.vendored.joml.Vector3d; @@ -20,6 +21,8 @@ public class IrisExclusiveUniforms { public static void addIrisExclusiveUniforms(UniformHolder uniforms) { WorldInfoUniforms.addWorldInfoUniforms(uniforms); + uniforms.uniform1i(UniformUpdateFrequency.PER_TICK, "currentColorSpace", () -> IrisVideoSettings.colorSpace.ordinal()); + //All Iris-exclusive uniforms (uniforms which do not exist in either OptiFine or ShadersMod) should be registered here. uniforms.uniform1f(UniformUpdateFrequency.PER_FRAME, "thunderStrength", IrisExclusiveUniforms::getThunderStrength); uniforms.uniform1f(UniformUpdateFrequency.PER_TICK, "currentPlayerHealth", IrisExclusiveUniforms::getCurrentHealth); diff --git a/src/main/resources/assets/iris/lang/en_us.json b/src/main/resources/assets/iris/lang/en_us.json index 1e9514d9f1..78ad670adb 100644 --- a/src/main/resources/assets/iris/lang/en_us.json +++ b/src/main/resources/assets/iris/lang/en_us.json @@ -57,6 +57,8 @@ "options.iris.shadowDistance.sodium_tooltip": "The shadow render distance controls how far away terrain can potentially be rendered in the shadow pass. Lower distances mean that less terrain will be rendered, improving frame rates. This option cannot be changed on packs which explicitly specify a shadow render distance. The actual shadow render distance is capped by the View Distance setting.", "options.iris.shadowDistance.enabled": "Allows you to change the maximum distance for shadows. Terrain and entities beyond this distance will not cast shadows. Lowering the shadow distance can significantly increase performance.", "options.iris.shadowDistance.disabled": "Your current shader pack has already set a render distance for shadows; you cannot change it.", + "options.iris.colorSpace": "Color Space", + "options.iris.colorSpace.sodium_tooltip": "The color space to transform the screen to. Works on top of shader packs.", "options.iris.gui.hide": "Hide GUI", "options.iris.gui.show": "Show GUI", diff --git a/src/main/resources/assets/iris/lang/ru_ru.json b/src/main/resources/assets/iris/lang/ru_ru.json index c81207797f..6a17ddb3f1 100644 --- a/src/main/resources/assets/iris/lang/ru_ru.json +++ b/src/main/resources/assets/iris/lang/ru_ru.json @@ -55,6 +55,8 @@ "options.iris.shadowDistance.enabled": "Позволяет изменять максимальное расстояние видимости теней. Блоки и сущности за его пределами не будут иметь теней. Снижая этот параметр, вы можете значительно улучшить производительность.", "options.iris.shadowDistance.disabled": "Ваш текущий набор шейдеров уже установил своё расстояние для теней; вы не сможете его изменить.", "options.iris.shadowDistance.sodium_tooltip": "Дальность теней определяет, насколько далеко местность может обрабатываться на этапе просчёта теней. Меньшие значения соответствуют меньшей площади, что улучшает частоту кадров. Этот параметр нельзя изменить для наборов, в которых установлено своё расстояние рендеринга теней. Фактическое расстояние рендеринга ограничено дальностью прорисовки.", + "options.iris.colorSpace": "Цветовое пространство", + "options.iris.colorSpace.sodium_tooltip": "Цветовое пространство применяемое на экране. Работает поверх включённых шейдеров.", "options.iris.gui.hide": "Скрыть", "options.iris.gui.show": "Показать", diff --git a/src/main/resources/assets/iris/lang/uk_ua.json b/src/main/resources/assets/iris/lang/uk_ua.json index 6c1a80b974..fdd4dfca55 100644 --- a/src/main/resources/assets/iris/lang/uk_ua.json +++ b/src/main/resources/assets/iris/lang/uk_ua.json @@ -50,6 +50,8 @@ "options.iris.shadowDistance.enabled": "Дозволяє змінити максимальну відстань для тіней. Місцевість та об’єкти за межами цієї відстані не відкидають тіні. Зменшення відстані тіні може значно підвищити продуктивність.", "options.iris.shadowDistance.disabled": "Ваш поточний пакет шейдерів уже встановив відстань візуалізації для тіней; ви не можете змінити це.", "options.iris.shadowDistance.sodium_tooltip": "Дальність тіней визначає, наскільки далеко місцевість може оброблятися на етапі прорахунку тіней. Найменші значення відповідають меншій площі, що покращує частоту кадрів. Цей параметр не можна змінити для шейдерів, у яких встановлена відстань рендерингу тіней. Фактична відстань рендерингу обмежена дальністю промальовування.", + "options.iris.colorSpace": "Колірний простір", + "options.iris.colorSpace.sodium_tooltip": "Колірний простір який застосовується на екрані. Працює поверх увімкнених шейдерів.", "pack.iris.select.title": "Виберіть Шейдери", "pack.iris.configure.title": "Налаштувати", diff --git a/src/main/resources/colorSpace.csh b/src/main/resources/colorSpace.csh new file mode 100644 index 0000000000..549b9176fb --- /dev/null +++ b/src/main/resources/colorSpace.csh @@ -0,0 +1,138 @@ +#ifdef COMPUTE +#version 430 core +layout(local_size_x = 8, local_size_y = 8) in; +#else +#version 330 core +#endif + +#ifdef COMPUTE +layout(rgba8) uniform image2D readImage; +#else +uniform sampler2D readImage; +in vec2 uv; +out vec3 outColor; +#endif + +// https://en.wikipedia.org/wiki/Rec._709#Transfer_characteristics +vec3 EOTF_Curve(vec3 LinearCV, const float LinearFactor, const float Exponent, const float Alpha, const float Beta) { + return mix(LinearCV * LinearFactor, clamp(Alpha * pow(LinearCV, vec3(Exponent)) - (Alpha - 1.0), 0.0, 1.0), step(Beta, LinearCV)); +} + +// https://en.wikipedia.org/wiki/SRGB#Transfer_function_(%22gamma%22) +vec3 EOTF_IEC61966(vec3 LinearCV) { + return EOTF_Curve(LinearCV, 12.92, 1.0 / 2.4, 1.055, 0.0031308);; + //return mix(LinearCV * 12.92, clamp(pow(LinearCV, vec3(1.0/2.4)) * 1.055 - 0.055, 0.0, 1.0), step(0.0031308, LinearCV)); +} +vec3 InverseEOTF_IEC61966(vec3 DisplayCV){ + return max(mix(DisplayCV / 12.92, pow(0.947867 * DisplayCV + 0.0521327, vec3(2.4)), step(0.04045, DisplayCV)), 0.0); +} + +// https://en.wikipedia.org/wiki/Rec._709#Transfer_characteristics +vec3 EOTF_BT709(vec3 LinearCV) { + return EOTF_Curve(LinearCV, 4.5, 0.45, 1.099, 0.018); + //return mix(LinearCV * 4.5, clamp(pow(LinearCV, vec3(0.45)) * 1.099 - 0.099, 0.0, 1.0), step(0.018, LinearCV)); +} + +// https://en.wikipedia.org/wiki/Rec._2020#Transfer_characteristics +vec3 EOTF_BT2020_12Bit(vec3 LinearCV) { + return EOTF_Curve(LinearCV, 4.5, 0.45, 1.0993, 0.0181); +} + +// https://en.wikipedia.org/wiki/DCI-P3 +vec3 EOTF_P3DCI(vec3 LinearCV) { + return pow(LinearCV, vec3(1.0 / 2.6)); +} +// https://en.wikipedia.org/wiki/Adobe_RGB_color_space +vec3 EOTF_Adobe(vec3 LinearCV) { + return pow(LinearCV, vec3(1.0 / 2.2)); +} + +// Using calculations from https://github.com/ampas/aces-dev as reference +const mat3 sRGB_XYZ = mat3( + 0.4124564, 0.3575761, 0.1804375, + 0.2126729, 0.7151522, 0.0721750, + 0.0193339, 0.1191920, 0.9503041 +); +/* +// This Matrix from the acesdev repo is incorrect and produces a strong red-shift +const mat3 XYZ_P3DCI = mat3( + 2.7253940305, -1.0180030062, -0.4401631952, + -0.7951680258, 1.6897320548, 0.0226471906, + 0.0412418914, -0.0876390192, 1.1009293786 +); +*/ +const mat3 XYZ_P3D65 = mat3( + 2.4933963, -0.9313459, -0.4026945, + -0.8294868, 1.7626597, 0.0236246, + 0.0358507, -0.0761827, 0.9570140 +); +const mat3 XYZ_REC2020 = mat3( + 1.7166511880, -0.3556707838, -0.2533662814, + -0.6666843518, 1.6164812366, 0.0157685458, + 0.0176398574, -0.0427706133, 0.9421031212 +); +// https://en.wikipedia.org/wiki/Adobe_RGB_color_space +const mat3 XYZ_AdobeRGB = mat3( + 2.04158790381075, -0.56500697427886, -0.34473135077833, + -0.96924363628088, 1.87596750150772, 0.0415550574071756, + 0.0134442806320311, -0.118362392231018, 1.01517499439121 +); + +// Bradford chromatic adaptation from standard D65 to DCI Cinema White +const mat3 D65_DCI = mat3( + 1.02449672775258, 0.0151635410224164, 0.0196885223342068, + 0.0256121933371582, 0.972586305624413, 0.00471635229242733, + 0.00638423065008769, -0.0122680827367302, 1.14794244517368 +); + +// Bradford chromatic adaptation between D60 and D65 +const mat3 D65_D60 = mat3( + 1.01303, 0.00610531, -0.014971, + 0.00769823, 0.998165, -0.00503203, + -0.00284131, 0.00468516, 0.924507 +); +const mat3 D60_D65 = mat3( + 0.987224, -0.00611327, 0.0159533, + -0.00759836, 1.00186, 0.00533002, + 0.00307257, -0.00509595, 1.08168 +); + +const mat3 sRGB_to_P3DCI = ((sRGB_XYZ) * XYZ_P3D65) * D65_DCI; +//const mat3 sRGB_to_P3DCI = (sRGB_XYZ) * (XYZ_P3DCI); +const mat3 sRGB_to_P3D65 = sRGB_XYZ * XYZ_P3D65; +const mat3 sRGB_to_REC2020 = sRGB_XYZ * XYZ_REC2020; +const mat3 sRGB_to_AdobeRGB = sRGB_XYZ * XYZ_AdobeRGB; +void main() { + #if CURRENT_COLOR_SPACE != SRGB + #ifdef COMPUTE + ivec2 PixelIndex = ivec2(gl_GlobalInvocationID.xy); + vec3 SourceColor = imageLoad(readImage, PixelIndex).rgb; + #else + vec3 SourceColor = texture(readImage, uv).rgb; + #endif + SourceColor = InverseEOTF_IEC61966(SourceColor); + vec3 TargetColor = SourceColor; + #if CURRENT_COLOR_SPACE == DCI_P3 + // https://en.wikipedia.org/wiki/DCI-P3 + TargetColor = TargetColor * sRGB_to_P3DCI; + TargetColor = EOTF_P3DCI(TargetColor); + #elif CURRENT_COLOR_SPACE == DISPLAY_P3 + // https://en.wikipedia.org/wiki/DCI-P3#Display_technology + TargetColor = TargetColor * sRGB_to_P3D65; + TargetColor = EOTF_IEC61966(TargetColor); + #elif CURRENT_COLOR_SPACE == REC2020 + // https://en.wikipedia.org/wiki/Rec._2020 + TargetColor = TargetColor * sRGB_to_REC2020; + TargetColor = EOTF_BT709(TargetColor); + #elif CURRENT_COLOR_SPACE == ADOBE_RGB + // https://en.wikipedia.org/wiki/Adobe_RGB_color_space + TargetColor = TargetColor * sRGB_to_AdobeRGB; + TargetColor = EOTF_Adobe(TargetColor); + #endif + #ifdef COMPUTE + imageStore(readImage, PixelIndex, vec4(TargetColor, 1.0)); + #else + outColor = TargetColor; + #endif + #endif +} \ No newline at end of file diff --git a/src/main/resources/colorSpace.vsh b/src/main/resources/colorSpace.vsh new file mode 100644 index 0000000000..5d0154f6ad --- /dev/null +++ b/src/main/resources/colorSpace.vsh @@ -0,0 +1,11 @@ +#version 330 core + +in vec3 iris_Position; +in vec2 iris_UV0; +uniform mat4 projection; +out vec2 uv; + +void main() { + gl_Position = projection * vec4(iris_Position, 1.0); + uv = iris_UV0; +} \ No newline at end of file