diff --git a/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java index 7f702e43..96d420f7 100644 --- a/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/RealLensOpenGlFrameCapturer.java @@ -50,7 +50,7 @@ public class RealLensOpenGlFrameCapturer final float GOLDEN_ANGLE = (float) (Math.PI * (3.0 - Math.sqrt(5.0))); - float random_offset = (float) Math.sqrt(r*r/n) * 0.5f; + float random_offset = (float) Math.sqrt(r*r/n) * 0.0f; for (int i = 0; i < n; i++) { float rr = r * (float) Math.sqrt((float) i / n); @@ -88,6 +88,24 @@ public class RealLensOpenGlFrameCapturer return cameras; } + private static Data singleDiskSample(int i, int n, float focalDistance, float apertureRadius) { + final float GOLDEN_ANGLE = (float) (Math.PI * (3.0 - Math.sqrt(5.0))); + + float rr = apertureRadius * (float) Math.sqrt((float) i / n); + float t = i * GOLDEN_ANGLE; + + float dx = rr * (float) Math.cos(t); + float dy = rr * (float) Math.sin(t); + float dz = 0f; + + Data cam = new Data(dx, dy, dz, 0f, 0f); + + float dirX = -dx, dirY = -dy, dirZ = focalDistance - dz; + cam.yaw = (float) -Math.toDegrees(Math.atan2(dirX, dirZ)); + cam.pitch = (float) Math.toDegrees(Math.atan2(dirY, Math.sqrt(dirX * dirX + dirZ * dirZ))); + return cam; + } + private final Path positionPath; private final Timeline timeline; private final ReplayHandler replayHandler; @@ -111,62 +129,66 @@ public class RealLensOpenGlFrameCapturer int frameId = framesDone++; RenderSettings settings = renderInfo.getRenderSettings(); - int fps = settings.getFramesPerSecond(); - int apertureSamples = settings.getLensBlurSamples(); - int subframes = Math.max(1, settings.getMotionBlurSubframes()); // M - float shutter = settings.getShutterAngle(); // градусы + int fps = settings.getFramesPerSecond(); + int N = Math.max(1, settings.getLensBlurSamples()); // единый бюджет сэмплов + float shutter = settings.getShutterAngle(); // градусы - double dt = 1000.0 / fps; // длительность выходного кадра, мс - // сколько суб-кадров затвор открыт (округляем до целого) - int openCount = Math.max(1, (int) Math.round(subframes * shutter / 360.0)); + boolean motionBlur = shutter > 0f; + double dt = 1000.0 / fps; // длительность выходного кадра, мс + double shutterFrac = motionBlur ? (shutter / 360.0) : 0.0; - int width = getFrameWidth(); + int width = getFrameWidth(); int height = getFrameHeight(); - int bpp = 4; + 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++; - } + // перемешанные слои времени, чтобы время не коррелировало с индексом апертурной спирали + int[] order = new int[N]; + for (int i = 0; i < N; i++) order[i] = i; + for (int i = N - 1; i > 0; i--) { // Fisher-Yates + int j = ThreadLocalRandom.current().nextInt(i + 1); + int tmp = order[i]; + order[i] = order[j]; + order[j] = tmp; + } + + for (int i = 0; i < N; i++) { + // --- время: 1 стратифицированный джиттернутый сэмпл на слой --- + long tSub; + if (motionBlur) { + double u = (i + ThreadLocalRandom.current().nextDouble()) / N; + tSub = (long) (frameId * dt + u * shutterFrac * dt); + timeline.applyToGame(tSub, replayHandler); + } else { + tSub = (long) (frameId * dt); + } + // partialTicks от РЕАЛЬНОГО времени реплея (frac(replayTime/50)), + // как в AbstractTimelinePlayer.onTick. (tSub % 50) даёт рассинхрон -> раздвоение. + long replayTime = replayHandler.getReplaySender().currentTimeStamp(); + float subPartialTicks = (float) ((replayTime % 50L) / 50.0); + + float focal = positionPath.getValue(LensProperties.FOCAL_DISTANCE, tSub).map(Triple::getLeft).orElse(5.0f); + float apert = positionPath.getValue(LensProperties.APERTURE_RADIUS, tSub).map(Triple::getLeft).orElse(0.05f); + Data cam = singleDiskSample(order[i], N, focal, apert); + + OpenGlFrame sub = renderFrame(frameId, subPartialTicks, cam); + ByteBuffer buf = sub.getByteBuffer(); + buf.rewind(); + for (int k = 0; k < totalBytes; k++) { + accumulator[k] += (buf.get(k) & 0xFF); + } + ByteBufferPool.release(buf); } - // усредняем в один кадр ByteBuffer out = ByteBufferPool.allocate(totalBytes); - for (int i = 0; i < totalBytes; i++) { - out.put((byte) (accumulator[i] / totalSamples)); + for (int k = 0; k < totalBytes; k++) { + out.put((byte) (accumulator[k] / N)); } out.rewind(); 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 })); + 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/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 768fd047..41a0189b 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -204,7 +204,6 @@ public class GuiRenderSettings extends AbstractGuiPopup { 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.motionblursubframes"), motionBlurSubframes, new GuiLabel().setI18nText("replaymod.gui.rendersettings.shutterangle"), shutterAngle)); public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command")