From 7ab2edd7386ecb2674a3b771289c9f0bcc0c75a6 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 29 Jun 2021 17:57:11 +0200 Subject: [PATCH] Implement ODS rendering via Iris Shader for MC 1.17 --- build.gradle | 4 + .../render/capturer/IrisODSFrameCapturer.java | 156 ++++++++++++++++++ .../replaymod/render/gui/GuiRenderQueue.java | 2 +- .../render/gui/GuiRenderSettings.java | 2 +- .../mixin/Mixin_AddIrisOdsShaderUniforms.java | 27 +++ .../mixin/Mixin_LoadIrisOdsShaderPack.java | 29 ++++ .../replaymod/render/rendering/Pipelines.java | 10 +- .../render/rendering/VideoRenderer.java | 19 ++- .../replay/gui/screen/GuiReplayViewer.java | 2 +- .../ods/shaders/gbuffers_textured_lit.fsh | 15 ++ .../ods/shaders/gbuffers_textured_lit.vsh | 69 ++++++++ .../resources/mixins.render.replaymod.json | 4 + 12 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java create mode 100644 src/main/java/com/replaymod/render/mixin/Mixin_AddIrisOdsShaderUniforms.java create mode 100644 src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java create mode 100644 src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.fsh create mode 100644 src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh diff --git a/build.gradle b/build.gradle index a22d6932..311f481f 100644 --- a/build.gradle +++ b/build.gradle @@ -352,6 +352,10 @@ dependencies { } } + if (mcVersion >= 11600) { + modCompileOnly 'com.github.IrisShaders:Iris:1.0.0' + } + testImplementation 'junit:junit:4.11' } diff --git a/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java new file mode 100644 index 00000000..a1d31055 --- /dev/null +++ b/src/main/java/com/replaymod/render/capturer/IrisODSFrameCapturer.java @@ -0,0 +1,156 @@ +//#if MC>=11600 +package com.replaymod.render.capturer; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.replaymod.render.RenderSettings; +import com.replaymod.render.frame.CubicOpenGlFrame; +import com.replaymod.render.frame.ODSOpenGlFrame; +import com.replaymod.render.frame.OpenGlFrame; +import com.replaymod.render.rendering.Channel; +import com.replaymod.render.rendering.FrameCapturer; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import net.coderbot.iris.Iris; +import net.coderbot.iris.config.IrisConfig; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static com.replaymod.core.versions.MCVer.popMatrix; +import static com.replaymod.core.versions.MCVer.pushMatrix; +import static com.replaymod.core.versions.MCVer.resizeMainWindow; +import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; + +public class IrisODSFrameCapturer implements FrameCapturer { + + public static final String SHADER_PACK_NAME = "assets/replaymod/iris/ods"; + public static IrisODSFrameCapturer INSTANCE; + private final CubicPboOpenGlFrameCapturer left, right; + private final String prevShaderPack; + private int direction; + private boolean isLeftEye; + + public IrisODSFrameCapturer(WorldRenderer worldRenderer, final RenderInfo renderInfo, int frameSize) { + RenderInfo fakeInfo = new RenderInfo() { + private int call; + private float partialTicks; + @Override + public ReadableDimension getFrameSize() { + return renderInfo.getFrameSize(); + } + + @Override + public int getFramesDone() { + return renderInfo.getFramesDone(); + } + + @Override + public int getTotalFrames() { + return renderInfo.getTotalFrames(); + } + + @Override + public float updateForNextFrame() { + if (call++ % 2 == 0) { + partialTicks = renderInfo.updateForNextFrame(); + } + return partialTicks; + } + + @Override + public RenderSettings getRenderSettings() { + return renderInfo.getRenderSettings(); + } + }; + left = new CubicStereoFrameCapturer(worldRenderer, fakeInfo, frameSize); + right = new CubicStereoFrameCapturer(worldRenderer, fakeInfo, frameSize); + + INSTANCE = this; + prevShaderPack = Iris.getIrisConfig().getShaderPackName().orElse(null); + setShaderPack(SHADER_PACK_NAME); + } + + private static void setShaderPack(String name) { + IrisConfig irisConfig = Iris.getIrisConfig(); + irisConfig.setShaderPackName(name); + try { + irisConfig.save(); + Iris.reload(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public int getDirection() { + return direction; + } + + public boolean isLeftEye() { + return isLeftEye; + } + + @Override + public boolean isDone() { + return left.isDone() && right.isDone(); + } + + @Override + public Map process() { + isLeftEye = true; + Map leftChannels = left.process(); + isLeftEye = false; + Map rightChannels = right.process(); + + if (leftChannels != null && rightChannels != null) { + Map result = new HashMap<>(); + for (Channel channel : Channel.values()) { + CubicOpenGlFrame leftFrame = leftChannels.get(channel); + CubicOpenGlFrame rightFrame = rightChannels.get(channel); + if (leftFrame != null && rightFrame != null) { + result.put(channel, new ODSOpenGlFrame(leftFrame, rightFrame)); + } + } + return result; + } + return null; + } + + @Override + public void close() throws IOException { + left.close(); + right.close(); + INSTANCE = null; + setShaderPack(prevShaderPack); + } + + private class CubicStereoFrameCapturer extends CubicPboOpenGlFrameCapturer { + public CubicStereoFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, int frameSize) { + super(worldRenderer, renderInfo, frameSize); + } + + @Override + protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) { + resizeMainWindow(mc, getFrameWidth(), getFrameHeight()); + + pushMatrix(); + frameBuffer().beginWrite(true); + + GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT + //#if MC>=11400 + , false + //#endif + ); + GlStateManager.enableTexture(); + + direction = captureData.ordinal(); + worldRenderer.renderWorld(partialTicks, null); + + frameBuffer().endWrite(); + popMatrix(); + + return captureFrame(frameId, captureData); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java index 0ab59405..8e9d92ea 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -348,7 +348,7 @@ public class GuiRenderQueue extends AbstractGuiPopup implements renderButton.setEnabled(jobs.size() > 0); renderButton.setI18nLabel("replaymod.gui.renderqueue.render" + (selected > 0 ? "selected" : "all")); - String[] compatError = VideoRenderer.checkCompat(); + String[] compatError = VideoRenderer.checkCompat(jobs.stream().map(RenderJob::getSettings)); if (compatError != null) { renderButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError)); } diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index b6eae92f..1d8b9c9a 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -363,7 +363,7 @@ public class GuiRenderSettings extends AbstractGuiPopup { videoHeight.setTextColor(Colors.RED); } - String[] compatError = VideoRenderer.checkCompat(); + String[] compatError = VideoRenderer.checkCompat(save(false)); if (resolutionError != null) { renderButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(resolutionError)); } else if (compatError != null) { diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_AddIrisOdsShaderUniforms.java b/src/main/java/com/replaymod/render/mixin/Mixin_AddIrisOdsShaderUniforms.java new file mode 100644 index 00000000..e034a547 --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/Mixin_AddIrisOdsShaderUniforms.java @@ -0,0 +1,27 @@ +//#if MC>=11600 +package com.replaymod.render.mixin; + +import com.replaymod.render.capturer.IrisODSFrameCapturer; +import net.coderbot.iris.gl.uniform.UniformHolder; +import net.coderbot.iris.gl.uniform.UniformUpdateFrequency; +import net.coderbot.iris.uniforms.CommonUniforms; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Pseudo +@Mixin(value = CommonUniforms.class, remap = false) +public class Mixin_AddIrisOdsShaderUniforms { + // Using ModifyVariable, so we only depend on a single argument. Hoping that reduces the chance of breaking changes. + @ModifyVariable(method = "generalCommonUniforms", at = @At("HEAD"), argsOnly = true) + private static UniformHolder addReplayModOdsUniforms(UniformHolder uniforms) { + IrisODSFrameCapturer ods = IrisODSFrameCapturer.INSTANCE; + if (ods != null) { + uniforms.uniform1b(UniformUpdateFrequency.PER_FRAME, "leftEye", ods::isLeftEye); + uniforms.uniform1i(UniformUpdateFrequency.PER_FRAME, "direction", ods::getDirection); + } + return uniforms; + } +} +//#endif diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java b/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java new file mode 100644 index 00000000..d8756e7b --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/Mixin_LoadIrisOdsShaderPack.java @@ -0,0 +1,29 @@ +//#if MC>=11600 +package com.replaymod.render.mixin; + +import com.replaymod.render.capturer.IrisODSFrameCapturer; +import net.coderbot.iris.Iris; +import net.fabricmc.loader.api.FabricLoader; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.nio.file.Path; + +@Pseudo +@Mixin(value = Iris.class, remap = false) +public class Mixin_LoadIrisOdsShaderPack { + @Redirect(method = "loadExternalShaderpack", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/coderbot/iris/Iris;SHADERPACKS_DIRECTORY:Ljava/nio/file/Path;")) + private static Path loadReplayModOdsPack(String name) { + if (IrisODSFrameCapturer.INSTANCE != null && IrisODSFrameCapturer.SHADER_PACK_NAME.equals(name)) { + return FabricLoader.getInstance().getModContainer("replaymod") + .orElseThrow(() -> new RuntimeException("Failed to get mod container for ReplayMod")) + .getRootPath(); + } else { + return Iris.SHADERPACKS_DIRECTORY; + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/render/rendering/Pipelines.java b/src/main/java/com/replaymod/render/rendering/Pipelines.java index 62a2b502..e012fb41 100644 --- a/src/main/java/com/replaymod/render/rendering/Pipelines.java +++ b/src/main/java/com/replaymod/render/rendering/Pipelines.java @@ -105,8 +105,14 @@ public class Pipelines { ODSToBitmapProcessor processor = new ODSToBitmapProcessor(settings.getVideoWidth(), settings.getVideoHeight(), settings.getSphericalFovX()); - FrameCapturer capturer = - new ODSFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize()); + //#if MC>=11600 + boolean iris = net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("iris"); + FrameCapturer capturer = iris + ? new com.replaymod.render.capturer.IrisODSFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize()) + : new ODSFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize()); + //#else + //$$ FrameCapturer capturer = new ODSFrameCapturer(worldRenderer, renderInfo, processor.getFrameSize()); + //#endif return new Pipeline<>(worldRenderer, capturer, processor, consumer); } diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 5ce80a9a..6baf7a28 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -74,10 +74,12 @@ import java.io.IOException; import java.util.Collection; import java.util.EnumMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; +import java.util.stream.Stream; import static com.google.common.collect.Iterables.getLast; import static com.replaymod.core.versions.MCVer.*; @@ -699,7 +701,11 @@ public class VideoRenderer implements RenderInfo { } } - public static String[] checkCompat() { + public static String[] checkCompat(Stream settings) { + return settings.map(VideoRenderer::checkCompat).filter(Objects::nonNull).findFirst().orElse(null); + } + + public static String[] checkCompat(RenderSettings settings) { //#if FABRIC>=1 if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("sodium")) { return new String[] { @@ -708,6 +714,17 @@ public class VideoRenderer implements RenderInfo { "For now, you need to uninstall Sodium before rendering!" }; } + //#if MC>=11700 + //$$ if (settings.getRenderMethod() == RenderSettings.RenderMethod.ODS + //$$ && !net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("iris")) { + //$$ return new String[] { + //$$ "ODS export requires Iris to be installed for Minecraft 1.17 and above.", + //$$ "Note that it is nevertheless incompatible with other shaders and will simply replace them.", + //$$ "Get it from: https://irisshaders.net/", + //$$ "Use the Standalone version! Sodium is not currently compatible with rendering!", + //$$ }; + //$$ } + //#endif //#endif return null; } diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index 7fe0e49f..bd889eb0 100644 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -260,7 +260,7 @@ public class GuiReplayViewer extends GuiScreen { loadButton.setTooltip(new GuiTooltip().setText(tooltipLines)); loadButton.setEnabled(!jobs.isEmpty()); - String[] compatError = VideoRenderer.checkCompat(); + String[] compatError = VideoRenderer.checkCompat(jobs.stream().map(RenderJob::getSettings)); if (compatError != null) { loadButton.setDisabled().setTooltip(new GuiTooltip().setText(compatError)); } diff --git a/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.fsh b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.fsh new file mode 100644 index 00000000..76b37f95 --- /dev/null +++ b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.fsh @@ -0,0 +1,15 @@ +#version 120 + +uniform sampler2D texture; +uniform sampler2D lightmap; + +varying vec4 color; +varying vec2 texcoord; +varying vec2 lmcoord; + +void main() { + vec4 lightmap = texture2D(lightmap, lmcoord); + vec4 color = texture2D(texture, texcoord) * color; + color *= lightmap; + gl_FragColor = color; +} \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh new file mode 100644 index 00000000..3b53c589 --- /dev/null +++ b/src/main/resources/assets/replaymod/iris/ods/shaders/gbuffers_textured_lit.vsh @@ -0,0 +1,69 @@ +#version 120 + +varying vec4 color; +varying vec2 texcoord; +varying vec2 lmcoord; + +uniform int leftEye; +uniform int direction; + +const float eyeDistance = 0.14; + +void main() { + // Transform to view space + vec4 position = gl_ModelViewMatrix * gl_Vertex; + + // Distort for ODS + // O := The origin + // P := The current vertex/point + // T := Point of tangency with the tangent going through P + // Distance between P and O + float distPO = sqrt(position.x * position.x + position.z * position.z); + float distTO = eyeDistance * 0.5; + // Angle between PO and PT + float angP = acos(distTO / distPO); + // Angle between PO and TO (angle at the origin) + float angO = 90.0 - angP; + if (leftEye == 0) { + angO = -angO; + } + // The angel of OP within the circle, that is between OP and O(0,1) + float angOP = atan(position.x, position.z); + // The angle of OT within the circle, that is between OT and O(0,1) + float angOT = angO + angOP; + // Calculate the vector between O and T and finally move the vertex by that vector + position -= vec4(distTO * sin(angOT), 0, distTO * cos(angOT), 0); + + // Rotate for different cubic views + float z; + if (direction == 0) { // LEFT + z = position.z; + position.z = position.x; + position.x = -z; + } else if (direction == 1) { // RIGHT + z = position.z; + position.z = -position.x; + position.x = z; + } else if (direction == 2) { // FRONT + // No changes required + } else if (direction == 3) { // BACK + position.x = -position.x; + position.z = -position.z; + } else if (direction == 4) { // TOP + z = position.z; + position.z = -position.y; + position.y = z; + } else if (direction == 5) { // BOTTOM + z = position.z; + position.z = position.y; + position.y = -z; + } + + // Transform to screen space + gl_Position = gl_ProjectionMatrix * position; + + // Misc. + color = gl_Color; + texcoord = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; + lmcoord = (gl_TextureMatrix[1] * gl_MultiTexCoord1).xy; +} \ No newline at end of file diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 05ba5423..7bddb362 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -20,6 +20,10 @@ "Mixin_StabilizeCamera", "Mixin_Stereoscopic_Camera", "Mixin_Stereoscopic_HandRenderPass", + //#if MC>=11600 + "Mixin_AddIrisOdsShaderUniforms", + "Mixin_LoadIrisOdsShaderPack", + //#endif //#if MC>=10800 //#if MC>=11500 "Mixin_BlockOnChunkRebuilds",