diff --git a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java index 37d313f0..54f22465 100644 --- a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java @@ -87,6 +87,11 @@ public abstract class OpenGlFrameCapturer process() { - // Продвигает состояние игры к времени ТЕКУЩЕГО выходного кадра - // (через onTick -> timeline.applyToGame(videoTime)). renderInfo.updateForNextFrame(); int frameId = framesDone++; RenderSettings settings = renderInfo.getRenderSettings(); - int fps = settings.getFramesPerSecond(); - int N = Math.max(1, settings.getLensBlurSamples()); // единый бюджет сэмплов - float shutter = settings.getShutterAngle(); // градусы + int fps = settings.getFramesPerSecond(); + int N = Math.max(1, settings.getLensBlurSamples()); + float shutter = settings.getShutterAngle(); boolean motionBlur = shutter > 0f; - double dt = 1000.0 / fps; // длительность выходного кадра, мс - double shutterFrac = motionBlur ? (shutter / 360.0) : 0.0; + 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 totalBytes = width * height * bpp; - long[] accumulator = new long[totalBytes]; - - // перемешанные слои времени, чтобы время не коррелировало с индексом апертурной спирали - 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 + // перемешанный апертурный индекс (декорреляция DOF <-> время) + int[] apertureOrder = new int[N]; + for (int i = 0; i < N; i++) apertureOrder[i] = i; + for (int i = N - 1; i > 0; i--) { int j = ThreadLocalRandom.current().nextInt(i + 1); - int tmp = order[i]; - order[i] = order[j]; - order[j] = tmp; + int tmp = apertureOrder[i]; apertureOrder[i] = apertureOrder[j]; apertureOrder[j] = tmp; } + accum.begin(width, height); // float-FBO нужного размера + очистка в 0 + for (int i = 0; i < N; i++) { - // --- время: 1 стратифицированный джиттернутый сэмпл на слой --- long tSub; if (motionBlur) { - double u = (i + ThreadLocalRandom.current().nextDouble()) / N; + 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 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); + Data cam = singleDiskSample(apertureOrder[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); + renderWorldOnly(subPartialTicks, cam); // мир -> главный FBO, без readback + accum.add(frameBuffer().getColorAttachment()); // прибавить на GPU } - ByteBuffer out = ByteBufferPool.allocate(totalBytes); - for (int k = 0; k < totalBytes; k++) { - out.put((byte) (accumulator[k] / N)); - } - out.rewind(); + ByteBuffer out = accum.finishAveraged(N); // ОДИН readback + деление на N + accum.unbind(); // вернуться в главный FBO - OpenGlFrame averaged = new OpenGlFrame(frameId, new Dimension(width, height), bpp, out); - return Collections.singletonMap(Channel.BRGA, new RealLensOpenGlFrame(new OpenGlFrame[]{averaged})); + OpenGlFrame averaged = new OpenGlFrame(frameId, new Dimension(width, height), 4, out); + return Collections.singletonMap(Channel.BRGA, new RealLensOpenGlFrame(new OpenGlFrame[]{ averaged })); + } + + @Override + public void close() throws java.io.IOException { + super.close(); + accum.close(); } } \ No newline at end of file diff --git a/src/main/java/com/replaymod/render/utils/AccumulationBuffer.java b/src/main/java/com/replaymod/render/utils/AccumulationBuffer.java new file mode 100644 index 00000000..5be141d0 --- /dev/null +++ b/src/main/java/com/replaymod/render/utils/AccumulationBuffer.java @@ -0,0 +1,127 @@ +package com.replaymod.render.utils; + +//#if MC<12105 +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.systems.VertexSorter; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.render.GameRenderer; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL30; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +public class AccumulationBuffer { + private int fbo = 0; + private int tex = 0; + private int width = 0; + private int height = 0; + + public void begin(int width, int height) { + if (fbo == 0 || width != this.width || height != this.height) { + allocate(width, height); + } + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + GL11.glViewport(0, 0, width, height); + GL11.glClearColor(0f, 0f, 0f, 0f); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT); + } + + private void allocate(int width, int height) { + close(); + this.width = width; + this.height = height; + + // Сырые GL-вызовы не обновляют кэш GlStateManager -> сохраняем и + // возвращаем привязки, иначе MC пропустит ребинд (текстура неба и т.п.). + int prevTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + int prevFbo = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); + + tex = GL11.glGenTextures(); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, tex); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_RGBA32F, width, height, 0, + GL11.GL_RGBA, GL11.GL_FLOAT, (ByteBuffer) null); + + fbo = GL30.glGenFramebuffers(); + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, + GL11.GL_TEXTURE_2D, tex, 0); + + GL11.glBindTexture(GL11.GL_TEXTURE_2D, prevTex); + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, prevFbo); + } + + public void add(int srcColorTex) { + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + GL11.glViewport(0, 0, width, height); + + RenderSystem.disableDepthTest(); + RenderSystem.depthMask(false); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_ONE, GL11.GL_ONE); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + + Matrix4f savedProj = RenderSystem.getProjectionMatrix(); + VertexSorter savedSorter = RenderSystem.getVertexSorting(); + RenderSystem.setProjectionMatrix(new Matrix4f(), VertexSorter.BY_Z); + RenderSystem.getModelViewStack().push(); + RenderSystem.getModelViewStack().loadIdentity(); + RenderSystem.applyModelViewMatrix(); + + RenderSystem.setShader(GameRenderer::getPositionTexProgram); + RenderSystem.setShaderTexture(0, srcColorTex); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder bb = tessellator.getBuffer(); + bb.begin(GL11.GL_QUADS, VertexFormats.POSITION_TEXTURE); + bb.vertex(-1f, -1f, 0f).texture(0f, 0f).next(); + bb.vertex( 1f, -1f, 0f).texture(1f, 0f).next(); + bb.vertex( 1f, 1f, 0f).texture(1f, 1f).next(); + bb.vertex(-1f, 1f, 0f).texture(0f, 1f).next(); + tessellator.draw(); + + RenderSystem.getModelViewStack().pop(); + RenderSystem.applyModelViewMatrix(); + RenderSystem.setProjectionMatrix(savedProj, savedSorter); + RenderSystem.disableBlend(); + RenderSystem.depthMask(true); + RenderSystem.enableDepthTest(); + } + + public ByteBuffer finishAveraged(int samples) { + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + + int pixels = width * height; + FloatBuffer fb = org.lwjgl.BufferUtils.createFloatBuffer(pixels * 4); + GL11.glReadPixels(0, 0, width, height, GL12.GL_BGRA, GL11.GL_FLOAT, fb); + + ByteBuffer out = ByteBufferPool.allocate(pixels * 4); + float inv = 1f / samples; + for (int i = 0; i < pixels * 4; i++) { + float v = fb.get(i) * inv; + int b = (int) (v * 255f + 0.5f); + if (b < 0) b = 0; else if (b > 255) b = 255; + out.put((byte) b); + } + out.rewind(); + return out; + } + + public void unbind() { + net.minecraft.client.MinecraftClient.getInstance().getFramebuffer().beginWrite(true); + } + + public void close() { + if (fbo != 0) { GL30.glDeleteFramebuffers(fbo); fbo = 0; } + if (tex != 0) { GL11.glDeleteTextures(tex); tex = 0; } + } +} +//#else +//$$ // TODO: путь под MC>=12105 (GpuDevice/CommandEncoder) — заполнить позже. +//#endif \ No newline at end of file