From 4e7c027ec09d58d90d046ff85f6ea65113f8003a Mon Sep 17 00:00:00 2001 From: DIvan2000 Date: Wed, 8 Jul 2026 02:13:44 +0400 Subject: [PATCH] add motion blur feature --- .../com/replaymod/render/RenderSettings.java | 14 +++ .../capturer/RealLensOpenGlFrameCapturer.java | 90 +++++++++++++++---- .../replaymod/render/gui/GuiExportFailed.java | 2 + .../render/gui/GuiRenderSettings.java | 15 +++- 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index 964ea2fb..76a03948 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -156,6 +156,8 @@ public class RenderSettings { private final boolean cameraPathExport; private final AntiAliasing antiAliasing; private final int lensBlurSamples; + private final int motionBlurSubframes; + private final float shutterAngle; private final String exportCommand; // We switched from rgb24 to bgra for performance at one point, so for backwards compatibility we need to @@ -191,6 +193,8 @@ public class RenderSettings { false, RenderSettings.AntiAliasing.NONE, 16, + 1, + 180f, "", RenderSettings.EncodingPreset.MP4_CUSTOM.getValue(), false @@ -218,6 +222,8 @@ public class RenderSettings { boolean cameraPathExport, AntiAliasing antiAliasing, int lensBlurSamples, + int motionBlurSubframes, + float shutterAngle, String exportCommand, String exportArguments, boolean highPerformance @@ -242,6 +248,8 @@ public class RenderSettings { this.cameraPathExport = cameraPathExport; this.antiAliasing = antiAliasing; this.lensBlurSamples = lensBlurSamples; + this.motionBlurSubframes = motionBlurSubframes; + this.shutterAngle = shutterAngle; this.exportCommand = exportCommand; this.exportArguments = exportArguments; this.highPerformance = highPerformance; @@ -269,6 +277,8 @@ public class RenderSettings { cameraPathExport, antiAliasing, lensBlurSamples, + motionBlurSubframes, + shutterAngle, exportCommand, exportArguments, highPerformance @@ -464,6 +474,10 @@ public class RenderSettings { public int getLensBlurSamples() { return lensBlurSamples; } + public int getMotionBlurSubframes() { return motionBlurSubframes; } + + public float getShutterAngle() { return shutterAngle; } + public String getExportCommand() { return exportCommand; } diff --git a/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java index 8c56b366..7f702e43 100644 --- a/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java @@ -8,6 +8,13 @@ import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.pathing.properties.LensProperties; +import com.replaymod.render.RenderSettings; +import com.replaymod.render.utils.ByteBufferPool; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.simplepathing.SPTimeline; + import org.apache.commons.lang3.tuple.Triple; import java.util.Collections; @@ -15,6 +22,10 @@ import java.util.Map; import java.util.concurrent.ThreadLocalRandom; +import java.nio.ByteBuffer; + +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; + public class RealLensOpenGlFrameCapturer extends OpenGlFrameCapturer { @@ -78,41 +89,84 @@ public class RealLensOpenGlFrameCapturer } private final Path positionPath; + private final Timeline timeline; + private final ReplayHandler replayHandler; public RealLensOpenGlFrameCapturer( WorldRenderer worldRenderer, RenderInfo renderInfo ) { super(worldRenderer, renderInfo); - this.positionPath = ReplayModSimplePathing.instance.getCurrentTimeline().getPositionPath(); + SPTimeline spTimeline = ReplayModSimplePathing.instance.getCurrentTimeline(); + this.positionPath = spTimeline.getPositionPath(); + this.timeline = spTimeline.getTimeline(); + this.replayHandler = ReplayModReplay.instance.getReplayHandler(); } @Override public Map process() { - float partialTicks = renderInfo.updateForNextFrame(); + // Продвигает состояние игры к времени ТЕКУЩЕГО выходного кадра + // (через onTick -> timeline.applyToGame(videoTime)). + renderInfo.updateForNextFrame(); int frameId = framesDone++; - int fps = renderInfo.getRenderSettings().getFramesPerSecond(); - long keyframeTime = (long) frameId * 1000 / fps; + RenderSettings settings = renderInfo.getRenderSettings(); + int fps = settings.getFramesPerSecond(); + int apertureSamples = settings.getLensBlurSamples(); + int subframes = Math.max(1, settings.getMotionBlurSubframes()); // M + float shutter = settings.getShutterAngle(); // градусы - float focalDistance = positionPath.getValue(LensProperties.FOCAL_DISTANCE, keyframeTime).map(Triple::getLeft).orElse(5.0f); - float apertureRadius = positionPath.getValue(LensProperties.APERTURE_RADIUS, keyframeTime).map(Triple::getLeft).orElse(0.05f); - int samples = renderInfo.getRenderSettings().getLensBlurSamples(); + double dt = 1000.0 / fps; // длительность выходного кадра, мс + // сколько суб-кадров затвор открыт (округляем до целого) + int openCount = Math.max(1, (int) Math.round(subframes * shutter / 360.0)); - if (positionPath == null) { - System.err.println("positionPath is null!"); - focalDistance = Float.MAX_VALUE; - apertureRadius = 0.01f; + int width = getFrameWidth(); + int height = getFrameHeight(); + int bpp = 4; + int totalBytes = width * height * bpp; + + // ОДИН аккумулятор на весь выходной кадр (сырой BGRA-порядок из glReadPixels) + long[] accumulator = new long[totalBytes]; + int totalSamples = 0; + + for (int j = 0; j < openCount; j++) { + // абсолютное время суб-кадра на таймлайне (мс), только вперёд + long tSub = (long) (frameId * dt + (j / (double) subframes) * dt); + + // пересэмплить камеру + сущности + FOV/линзу на суб-времени + timeline.applyToGame(tSub, replayHandler); + + // partialTicks для интерполяции сущностей внутри тика (50 мс) + float subPartialTicks = (float) ((tSub % 50L) / 50.0); + + // DOF-параметры берём тоже на суб-времени, чтобы фокус ехал во время смаза + float focalDistance = positionPath.getValue(LensProperties.FOCAL_DISTANCE, tSub).map(Triple::getLeft).orElse(5.0f); + float apertureRadius = positionPath.getValue(LensProperties.APERTURE_RADIUS, tSub).map(Triple::getLeft).orElse(0.05f); + + Data[] cameras = calculateCameras(focalDistance, apertureRadius, apertureSamples); + + for (Data cam : cameras) { + OpenGlFrame sub = renderFrame(frameId, subPartialTicks, cam); + ByteBuffer buf = sub.getByteBuffer(); + buf.rewind(); + for (int i = 0; i < totalBytes; i++) { + accumulator[i] += (buf.get(i) & 0xFF); + } + ByteBufferPool.release(buf); // сразу освобождаем -> нет OOM + totalSamples++; + } } - Data[] cameras = calculateCameras(focalDistance, apertureRadius, samples); - - OpenGlFrame[] frames = new OpenGlFrame[cameras.length]; - - for (int i = 0; i < cameras.length; i++) { - frames[i] = renderFrame(frameId, partialTicks, cameras[i]); + // усредняем в один кадр + ByteBuffer out = ByteBufferPool.allocate(totalBytes); + for (int i = 0; i < totalBytes; i++) { + out.put((byte) (accumulator[i] / totalSamples)); } + out.rewind(); - return Collections.singletonMap(Channel.BRGA, new RealLensOpenGlFrame(frames)); + OpenGlFrame averaged = new OpenGlFrame(frameId, new Dimension(width, height), bpp, out); + // Оборачиваем в RealLensOpenGlFrame с ОДНИМ кадром — процессор его пропустит через + // openGlBytesToBitmap (flip + swap R/B) и поделит на 1. Результат идентичен старому DOF. + return Collections.singletonMap(Channel.BRGA, new RealLensOpenGlFrame(new OpenGlFrame[]{ averaged })); } } \ No newline at end of file diff --git a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java index af1d2560..ed26b05d 100644 --- a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java +++ b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java @@ -102,6 +102,8 @@ public class GuiExportFailed extends GuiScreen { oldSettings.isCameraPathExport(), oldSettings.getAntiAliasing(), oldSettings.getLensBlurSamples(), + oldSettings.getMotionBlurSubframes(), + oldSettings.getShutterAngle(), oldSettings.getExportCommand(), oldSettings.getEncodingPreset().getValue(), oldSettings.isHighPerformance() diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index ba05f052..768fd047 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -104,6 +104,8 @@ public class GuiRenderSettings extends AbstractGuiPopup { public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true); public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true); public final GuiNumberField lensBlurSamples = new GuiNumberField().setSize(50, 20).setMinValue(1).setMaxValue(512).setValidateOnFocusChange(true); + public final GuiNumberField motionBlurSubframes = new GuiNumberField().setSize(50, 20).setMinValue(1).setMaxValue(512).setValidateOnFocusChange(true); + public final GuiNumberField shutterAngle = new GuiNumberField().setSize(50, 20).setMinValue(0).setMaxValue(360).setValidateOnFocusChange(true); public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() { @Override public void run() { @@ -201,7 +203,9 @@ public class GuiRenderSettings extends AbstractGuiPopup { depthMap, new GuiLabel(), cameraPathExport, new GuiLabel(), new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown, - new GuiLabel().setI18nText("replaymod.gui.rendersettings.lensblursamples"), lensBlurSamples)); + new GuiLabel().setI18nText("replaymod.gui.rendersettings.lensblursamples"), lensBlurSamples, + new GuiLabel().setI18nText("replaymod.gui.rendersettings.motionblursubframes"), motionBlurSubframes, + new GuiLabel().setI18nText("replaymod.gui.rendersettings.shutterangle"), shutterAngle)); public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command") .setSize(55, 20).setMaxLength(100).onTextChanged((old) -> updateInputs()); @@ -418,7 +422,10 @@ public class GuiRenderSettings extends AbstractGuiPopup { boolean commandChanged = !exportCommand.getText().isEmpty(); boolean argsChanged = !encodingPresetDropdown.getSelectedValue().getValue().equals(exportArguments.getText()); exportReset.setEnabled(commandChanged || argsChanged); - lensBlurSamples.setEnabled(renderMethod == RenderSettings.RenderMethod.REALLENS); + boolean isRealLens = renderMethod == RenderSettings.RenderMethod.REALLENS; + lensBlurSamples.setEnabled(isRealLens); + motionBlurSubframes.setEnabled(isRealLens); + shutterAngle.setEnabled(isRealLens); } protected String updateResolution() { @@ -546,6 +553,8 @@ public class GuiRenderSettings extends AbstractGuiPopup { cameraPathExport.setChecked(settings.isCameraPathExport()); antiAliasingDropdown.setSelected(settings.getAntiAliasing()); lensBlurSamples.setValue(settings.getLensBlurSamples()); + motionBlurSubframes.setValue(settings.getMotionBlurSubframes()); + shutterAngle.setValue((int) settings.getShutterAngle()); exportCommand.setText(settings.getExportCommand()); String exportArguments = settings.getExportArguments(); if (exportArguments == null || settings.getEncodingPreset() == null || invalidEncodingPreset) { @@ -580,6 +589,8 @@ public class GuiRenderSettings extends AbstractGuiPopup { cameraPathExport.isChecked(), serialize || antiAliasingDropdown.isEnabled() ? antiAliasingDropdown.getSelectedValue() : RenderSettings.AntiAliasing.NONE, lensBlurSamples.getInteger(), + motionBlurSubframes.getInteger(), + (float) shutterAngle.getInteger(), exportCommand.getText(), exportArguments.getText(), highPerformance