diff --git a/src/main/java/com/replaymod/core/LoadingPlugin.java b/src/main/java/com/replaymod/core/LoadingPlugin.java index a633a6bf..af5af130 100755 --- a/src/main/java/com/replaymod/core/LoadingPlugin.java +++ b/src/main/java/com/replaymod/core/LoadingPlugin.java @@ -18,6 +18,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin { MixinBootstrap.init(); Mixins.addConfiguration("mixins.recording.replaymod.json"); Mixins.addConfiguration("mixins.render.replaymod.json"); + Mixins.addConfiguration("mixins.render.blend.replaymod.json"); Mixins.addConfiguration("mixins.replay.replaymod.json"); Mixins.addConfiguration("mixins.compat.mapwriter.replaymod.json"); Mixins.addConfiguration("mixins.compat.shaders.replaymod.json"); diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index 3c3f3a9b..0f2ed434 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -7,6 +7,8 @@ import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.model.ModelBox; +import net.minecraft.client.model.ModelRenderer; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.ServerList; import net.minecraft.client.multiplayer.WorldClient; @@ -236,6 +238,11 @@ public class MCVer { return chunk.getEntityLists(); } + @SuppressWarnings("unchecked") + public static List cubeList(ModelRenderer modelRenderer) { + return modelRenderer.cubeList; + } + @SuppressWarnings("unchecked") public static List playerEntities(World world) { return world.playerEntities; diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java index 6fe7af62..f7f29e77 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java @@ -1,6 +1,7 @@ package com.replaymod.extras.advancedscreenshots; import com.replaymod.render.RenderSettings; +import com.replaymod.render.blend.BlendState; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.hooks.ChunkLoadingRenderGlobal; import com.replaymod.render.rendering.Pipelines; @@ -27,8 +28,13 @@ public class ScreenshotRenderer implements RenderInfo { ChunkLoadingRenderGlobal clrg = new ChunkLoadingRenderGlobal(mc.renderGlobal); - Pipelines.newPipeline(settings.getRenderMethod(),this, - new ScreenshotWriter(settings.getOutputFile())).run(); + if (settings.getRenderMethod() == RenderSettings.RenderMethod.BLEND) { + BlendState.setState(new BlendState(settings.getOutputFile())); + Pipelines.newBlendPipeline(this).run(); + } else { + Pipelines.newPipeline(settings.getRenderMethod(), this, + new ScreenshotWriter(settings.getOutputFile())).run(); + } clrg.uninstall(); diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index 37977dda..60fff513 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -11,7 +11,7 @@ import java.io.File; @Data public class RenderSettings { public enum RenderMethod { - DEFAULT, STEREOSCOPIC, CUBIC, EQUIRECTANGULAR, ODS; + DEFAULT, STEREOSCOPIC, CUBIC, EQUIRECTANGULAR, ODS, BLEND; @Override public String toString() { @@ -36,6 +36,8 @@ public class RenderSettings { MKV_LOSSLESS("-an -c:v libx264 -preset ultrafast -qp 0 \"%FILENAME%\"", "mkv"), + BLEND(null, "blend"), + PNG("\"%FILENAME%-%06d.png\"", "png"); private final String preset; diff --git a/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java b/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java new file mode 100644 index 00000000..71378bc9 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java @@ -0,0 +1,48 @@ +package com.replaymod.render.blend; + +import com.replaymod.render.capturer.RenderInfo; +import com.replaymod.render.capturer.WorldRenderer; +import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.rendering.FrameCapturer; +import com.replaymod.render.utils.ByteBufferPool; +import net.minecraft.client.Minecraft; +import org.lwjgl.util.Dimension; + +import java.io.IOException; + +public class BlendFrameCapturer implements FrameCapturer { + protected final WorldRenderer worldRenderer; + protected final RenderInfo renderInfo; + protected int framesDone; + + public BlendFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) { + this.worldRenderer = worldRenderer; + this.renderInfo = renderInfo; + } + + @Override + public boolean isDone() { + return framesDone >= renderInfo.getTotalFrames(); + } + + @Override + public RGBFrame process() { + if (framesDone == 0) { + BlendState.getState().setup(); + } + + renderInfo.updateForNextFrame(); + + BlendState.getState().preFrame(framesDone); + worldRenderer.renderWorld(Minecraft.getMinecraft().timer.renderPartialTicks, null); + BlendState.getState().postFrame(framesDone); + + return new RGBFrame(framesDone++, new Dimension(0, 0), ByteBufferPool.allocate(0)); + } + + @Override + public void close() throws IOException { + BlendState.getState().tearDown(); + BlendState.setState(null); + } +} diff --git a/src/main/java/com/replaymod/render/blend/BlendMaterials.java b/src/main/java/com/replaymod/render/blend/BlendMaterials.java new file mode 100644 index 00000000..4570d30b --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/BlendMaterials.java @@ -0,0 +1,70 @@ +package com.replaymod.render.blend; + +import com.replaymod.render.blend.data.DImage; +import com.replaymod.render.blend.data.DMaterial; +import com.replaymod.render.blend.data.DPackedFile; +import com.replaymod.render.blend.data.DTexture; +import net.minecraft.client.renderer.GLAllocation; +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.opengl.GL11; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +public class BlendMaterials { + private final Map materials = new HashMap<>(); + + public DMaterial getActiveMaterial() { + int textureId = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + DMaterial material = materials.get(textureId); + if (material == null) { + // Read raw image data from GL + int width = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_WIDTH); + int height = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_HEIGHT); + ByteBuffer buffer = GLAllocation.createDirectByteBuffer(width * height * 4); + GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); + + // Convert to BufferedImage + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); + byte[] data = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); + for (int i = 0; i < data.length; i += 4) { + data[i + 3] = buffer.get(); + data[i + 2] = buffer.get(); + data[i + 1] = buffer.get(); + data[i ] = buffer.get(); + } + + // Encode as png image + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + ImageIO.write(bufferedImage, "png", stream); + } catch (IOException e) { + throw new RuntimeException(e); // never happens unless ImageIO is bugged + } + byte[] bytes = stream.toByteArray(); + + // Wrap into blender structs + DImage image = new DImage(); + image.id.name = "texture.png"; + image.filePath = "texture.png"; + image.packedFiles.add(Pair.of("texture.png", new DPackedFile(bytes))); + + DTexture texture = new DTexture(); + texture.image = image; + + DMaterial.DMTex mTex = new DMaterial.DMTex(); + mTex.texture = texture; + + material = new DMaterial(); + material.textures.add(mTex); + materials.put(textureId, material); + } + return material; + } +} diff --git a/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java b/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java new file mode 100644 index 00000000..fcbb8380 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java @@ -0,0 +1,215 @@ +package com.replaymod.render.blend; + +import com.replaymod.render.blend.data.DMaterial; +import com.replaymod.render.blend.data.DMesh; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.vertex.VertexFormat; +import net.minecraft.client.renderer.vertex.VertexFormatElement; +import org.lwjgl.opengl.GL11; +import org.lwjgl.util.vector.Vector2f; +import org.lwjgl.util.vector.Vector3f; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +public class BlendMeshBuilder extends WorldRenderer { + private final DMesh mesh; + private final List vertices = new ArrayList<>(); + private final List uvs = new ArrayList<>(); + private final List colors = new ArrayList<>(); + private Vector3f offset = new Vector3f(0, 0, 0); + private boolean isDrawing; + private boolean wellBehaved; + private int mode; + private float u, v; + private int color = 0xffffffff; // Default not in parent class but probably set implicitly somewhere due to re-use + + public BlendMeshBuilder(DMesh mesh) { + super(0); + this.mesh = mesh; + } + + public void setWellBehaved(boolean wellBehaved) { + this.wellBehaved = wellBehaved; + } + + public void setOffset(Vector3f offset) { + this.offset = offset; + } + + @Override + public void startDrawing(int mode) { + if (isDrawing) { + if (!wellBehaved) { + // Someone probably finished drawing with the global instance instead of this one, + // let's just assume that what's happened and finish our last draw by ourselves + // (might miss correct texture though) + doFinish(); + } else { + throw new IllegalStateException("Already drawing!"); + } + } + this.isDrawing = true; + this.mode = mode; + + if (!wellBehaved) { + // In case the calling code finishes with Tessellator.getInstance().draw() + Tessellator.getInstance().getWorldRenderer().startDrawingQuads(); + } + } + + public void maybeFinishDrawing() { + if (isDrawing) { + doFinish(); + } + } + + @Override + public int finishDrawing() { + if (!isDrawing) { + throw new IllegalStateException("Not building!"); + } else { + if (!wellBehaved) { + Tessellator.getInstance().getWorldRenderer().finishDrawing(); + } + + doFinish(); + return -1; + } + } + + private void doFinish() { + this.isDrawing = false; + + DMaterial activeMaterial = BlendState.getState().getMaterials().getActiveMaterial(); + int materialSlot = mesh.materials.indexOf(activeMaterial); + if (materialSlot < 0) { + materialSlot = mesh.materials.size(); + mesh.materials.add(activeMaterial); + } + + switch (mode) { + case GL11.GL_TRIANGLES: + for (int i = 0; i < vertices.size(); i+=3) { + mesh.addTriangle( + vertices.get(i ), + vertices.get(i + 1), + vertices.get(i + 2), + uvs.get(i ), + uvs.get(i + 1), + uvs.get(i + 2), + colors.get(i ), + colors.get(i + 1), + colors.get(i + 2), + materialSlot + ); + } + break; + case GL11.GL_QUADS: + for (int i = 0; i < vertices.size(); i+=4) { + mesh.addQuad( + vertices.get(i ), + vertices.get(i + 1), + vertices.get(i + 2), + vertices.get(i + 3), + uvs.get(i ), + uvs.get(i + 1), + uvs.get(i + 2), + uvs.get(i + 3), + colors.get(i ), + colors.get(i + 1), + colors.get(i + 2), + colors.get(i + 3), + materialSlot + ); + } + break; + default: + throw new UnsupportedOperationException("Unsupported mode: " + mode); + } + } + + @Override + public void setTextureUV(double u, double v) { + this.u = (float) u; + this.v = (float) (1 - v); + } + + @Override + public void setColorRGBA(int r, int g, int b, int a) { + this.color = r | (g << 8) | (b << 16) | (a << 24); + } + + @Override + public void addVertex(double x, double y, double z) { + x -= offset.x; + y -= offset.y; + z -= offset.z; + vertices.add(new DMesh.Vertex((float) x, (float) -z, (float) y)); + uvs.add(new Vector2f(u, v)); + colors.add(color); + } + + @Override + @SuppressWarnings("unchecked") + public void addVertexData(int[] ints) { + ByteBuffer buffer = ByteBuffer.allocate(ints.length * 4).order(ByteOrder.nativeOrder()); + IntBuffer intBuffer = buffer.asIntBuffer(); + intBuffer.put(ints); + + VertexFormat vertexFormat = getVertexFormat(); + int posOffset = -1, colorOffset = -1, uvOffset = -1; + List elements = vertexFormat.getElements(); + for (VertexFormatElement element : elements) { + switch (element.getUsage()) { + case POSITION: posOffset = element.getOffset(); break; + case COLOR: colorOffset = element.getOffset(); break; + case UV: if (element.getIndex() == 0) uvOffset = element.getOffset(); break; + } + } + if (posOffset == -1) throw new IllegalStateException("No position element in " + vertexFormat); + if (uvOffset == -1) throw new IllegalStateException("No uv element in " + vertexFormat); + + for (int offset = 0; offset < buffer.remaining(); offset += vertexFormat.getNextOffset()) { + // TODO doesn't check buffer element types (works for vanilla because they're always the same) + if (colorOffset != -1) { + color = buffer.getInt(offset + colorOffset); + } + setTextureUV( + buffer.getFloat(offset + uvOffset), + buffer.getFloat(offset + uvOffset + 4) + ); + addVertex( + buffer.getFloat(offset + posOffset ), + buffer.getFloat(offset + posOffset + 4), + buffer.getFloat(offset + posOffset + 8) + ); + } + } + + @Override + public int getVertexCount() { + return vertices.size(); + } + + public void putColorRGBA(int byteIndex, int r, int g, int b, int a) { + int color = r | (g << 8) | (b << 16) | (a << 24); + putColor(color, -1); // FIXME: reverse index calculation + } + + private void putColor(int color, int reverseIndex) { + colors.set(colors.size() - reverseIndex, color); + } + + public void putColor4(int color) { + for (int i = 0; i < 4; i++) { + putColor(color, i + 1); + } + } + + // FIXME overwrite putX and more +} diff --git a/src/main/java/com/replaymod/render/blend/BlendState.java b/src/main/java/com/replaymod/render/blend/BlendState.java new file mode 100644 index 00000000..2c7ba4f0 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/BlendState.java @@ -0,0 +1,170 @@ +package com.replaymod.render.blend; + +import com.replaymod.render.blend.data.DScene; +import com.replaymod.render.blend.data.Serializer; +import com.replaymod.render.blend.exporters.ChunkExporter; +import com.replaymod.render.blend.exporters.EntityExporter; +import com.replaymod.render.blend.exporters.ItemExporter; +import com.replaymod.render.blend.exporters.ParticlesExporter; +import com.replaymod.render.blend.exporters.RenderState; +import com.replaymod.render.blend.exporters.TileEntityExporter; +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ReportedException; +import org.apache.commons.io.output.NullOutputStream; +import org.blender.utils.BlenderFactory; +import org.cakelab.blender.io.BlenderFile; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import static com.replaymod.render.ReplayModRender.LOGGER; + +// Note: +// - Chunk exporter currently assumes VBO are enabled and in use +public class BlendState implements Exporter { + private static BlendState currentState; + + public static void setState(BlendState state) { + currentState = state; + } + + public static BlendState getState() { + return currentState; + } + + private final List exporters = new ArrayList<>(); + private final BlenderFile blenderFile; + private final BlenderFactory factory; + private final DScene scene = new DScene(); + private final BlendMaterials materials = new BlendMaterials(); + + public BlendState(File file) throws IOException { + this.blenderFile = BlenderFactory.newBlenderFile(file); + this.factory = new BlenderFactory(blenderFile); + + RenderState renderState = new RenderState(this); + register(renderState); + register(new ChunkExporter()); + register(new EntityExporter(renderState)); + register(new TileEntityExporter(renderState)); + register(new ParticlesExporter(renderState)); + register(new ItemExporter(renderState)); + } + + public void register(Exporter exporter) { + exporters.add(exporter); + } + + public T get(Class clazz) { + for (Exporter exporter : exporters) { + if (clazz.isInstance(exporter)) { + return clazz.cast(exporter); + } + } + throw new NoSuchElementException("No exporter of type " + clazz); + } + + @Override + public void setup() { + for (Exporter exporter : exporters) { + try { + exporter.setup(); + } catch (IOException e) { + CrashReport report = CrashReport.makeCrashReport(e, "Setup of blend exporter"); + CrashReportCategory category = report.makeCategory("Exporter"); + category.addCrashSectionCallable("Exporter", exporter::toString); + throw new ReportedException(report); + } + } + } + + @Override + public void tearDown() { + for (Exporter exporter : exporters) { + try { + exporter.tearDown(); + } catch (IOException e) { + CrashReport report = CrashReport.makeCrashReport(e, "Tear down of blend exporter"); + CrashReportCategory category = report.makeCategory("Exporter"); + category.addCrashSectionCallable("Exporter", exporter::toString); + throw new ReportedException(report); + } + } + + try { + Serializer serializer = new Serializer(); + this.scene.serialize(serializer); + // TODO pre-configure render settings serializer.writeMajor(new Object(), new DId(BlockCodes.ID_REND)) + + //noinspection UseOfSystemOutOrSystemErr + PrintStream sysout = System.out; + try { + System.setOut(new PrintStream(new NullOutputStream())); + blenderFile.write(); + } finally { + System.setOut(sysout); + } + } catch (IOException e) { + LOGGER.error("Exception writing blend file: ", e); + } finally { + try { + blenderFile.close(); + } catch (IOException e) { + LOGGER.error("Exception closing blend file: ", e); + } + } + } + + @Override + public void preFrame(int frame) { + for (Exporter exporter : exporters) { + try { + exporter.preFrame(frame); + } catch (IOException e) { + CrashReport report = CrashReport.makeCrashReport(e, "Pre frame of blend exporter"); + CrashReportCategory category = report.makeCategory("Exporter"); + category.addCrashSectionCallable("Exporter", exporter::toString); + category.addCrashSection("Frame", frame); + throw new ReportedException(report); + } + } + } + + @Override + public void postFrame(int frame) { + for (Exporter exporter : exporters) { + try { + exporter.postFrame(frame); + } catch (IOException e) { + CrashReport report = CrashReport.makeCrashReport(e, "Post frame of blend exporter"); + CrashReportCategory category = report.makeCategory("Exporter"); + category.addCrashSectionCallable("Exporter", exporter::toString); + category.addCrashSection("Frame", frame); + throw new ReportedException(report); + } + } + + scene.endFrame = frame; + } + + public BlenderFile getBlenderFile() { + return blenderFile; + } + + public BlenderFactory getFactory() { + return factory; + } + + public DScene getScene() { + return scene; + } + + public BlendMaterials getMaterials() { + return materials; + } +} diff --git a/src/main/java/com/replaymod/render/blend/Exporter.java b/src/main/java/com/replaymod/render/blend/Exporter.java new file mode 100644 index 00000000..ce5f96c6 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/Exporter.java @@ -0,0 +1,10 @@ +package com.replaymod.render.blend; + +import java.io.IOException; + +public interface Exporter { + default void setup() throws IOException {} + default void tearDown() throws IOException {} + default void preFrame(int frame) throws IOException {} + default void postFrame(int frame) throws IOException {} +} diff --git a/src/main/java/com/replaymod/render/blend/ToJson.java b/src/main/java/com/replaymod/render/blend/ToJson.java new file mode 100644 index 00000000..063e0d41 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/ToJson.java @@ -0,0 +1,209 @@ +package com.replaymod.render.blend; + +import org.blender.utils.MainLib; +import org.cakelab.blender.generator.typemap.Renaming; +import org.cakelab.blender.io.BlenderFile; +import org.cakelab.blender.io.FileVersionInfo; +import org.cakelab.blender.io.block.Block; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.io.block.BlockTable; +import org.cakelab.blender.metac.CMetaModel; +import org.cakelab.blender.metac.CStruct; +import org.cakelab.blender.nio.CArrayFacade; +import org.cakelab.blender.nio.CFacade; +import org.cakelab.blender.nio.CPointer; +import org.cakelab.json.JSONArray; +import org.cakelab.json.JSONObject; +import org.cakelab.json.codec.JSONCodec; +import org.cakelab.json.codec.JSONCodecConfiguration; +import org.cakelab.json.codec.JSONCodecException; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ToJson { + + private static final String PACKAGE = "org.blender.dna"; + private static JSONCodec codec; + private static CMetaModel model; + private static BlenderFile blend; + private static BlockTable blockTable; + + public static void main(String[] args) throws IOException, JSONCodecException { + File fBlend = new File("/home/user/1.blend"); + //File fBlend = new File("test.blend"); + blend = new BlenderFile(fBlend); + + FileVersionInfo versions = blend.readFileGlobal(); + if (!MainLib.doVersionCheck(versions)) { + System.err.println("Warning: Conversion will probably fail due to version mismatch!"); + } + + blockTable = blend.getBlockTable(); + model = blend.getMetaModel(); + codec = new JSONCodec(new JSONCodecConfiguration(false)); + + JSONObject json = new JSONObject(); + JSONObject oHeader = createHeader(blend); + json.put("header", oHeader); + JSONArray aBlocks = new JSONArray(); + json.put("blocks", aBlocks); + for (Block b : blend.getBlocks()) { + addBlock(aBlocks, b); + } + blend.close(); + + File fJsonOut = new File(fBlend.getParentFile(), fBlend.getName().replace(".blend", ".json")); + FileOutputStream fout = new FileOutputStream(fJsonOut); + codec.encodeObject(json, fout); + fout.close(); + System.out.println("Finished"); + } + + private static JSONObject createHeader(BlenderFile blend) { + JSONObject oHeader = new JSONObject(); + oHeader.put("MAGIC", "BLENDER"); + oHeader.put("addressWidth", blend.getEncoding().getAddressWidth()); + oHeader.put("endianess", blend.getEncoding().getByteOrder().toString()); + oHeader.put("version", blend.getVersion().toString()); + return oHeader; + } + + private static void addBlock(JSONArray aBlocks, Block b) throws JSONCodecException, IOException { + JSONObject oBlock = new JSONObject(); + aBlocks.add(oBlock); + JSONObject header = (JSONObject) codec.encodeObjectJSON(b.header); + oBlock.put("header", header); + header.put("code", b.header.getCode().toString()); + CStruct struct = model.getStruct(b.header.getSdnaIndex()); + header.put("sdnaIndex", struct.getSignature() + "(" + b.header.getSdnaIndex() + ")"); + + System.out.print("[" + b.header.getAddress() + ", " + (b.header.getAddress() + b.header.getSize()) + "] "); + if (b.header.getCode().equals(BlockCodes.ID_DATA) && b.header.getSdnaIndex() == 0) { + System.out.println(b.header.getCode().toString() + ": Link(0) or undef"); + byte[] buffer = new byte[b.header.getSize()]; + b.data.readFully(buffer); + String str = toHexStr(buffer); + oBlock.put("raw", str); + int requiredSize = b.header.getCount() * struct.sizeof(blend.getEncoding().getAddressWidth()); + if (requiredSize == b.header.getSize()) { + addStruct(oBlock, b, struct); + } + } else { + System.out.println(b.header.getCode().toString() + ": " + struct.getSignature()); + addStruct(oBlock, b, struct); + } + } + + private static String toHexStr(byte[] buffer) { + StringBuffer str = new StringBuffer(); + for (byte v : buffer) { + String s = Integer.toHexString(v&0xff); + if (s.length() == 1) s = "0"+s; + str.append(s); + } + + return str.toString(); + } + + private static void addStruct(JSONObject oBlock, Block b, CStruct struct) { + if (b.header.getCount() == 1) { + oBlock.put(struct.getSignature(), createStruct(b, 0, struct)); + } else { + JSONArray aStructs = new JSONArray(); + oBlock.put(struct.getSignature() + "_array", aStructs); + for (int i = 0; i < b.header.getCount(); i++) { + aStructs.add(createStruct(b, i, struct)); + } + } + } + + private static Object createStruct(Block b, int index, CStruct struct) { + JSONObject oStruct = null; + + try { + Class cStruct = Class.forName(PACKAGE + "." + Renaming.mapStruct2Class(struct.getSignature())); + long address = b.header.getAddress() + index * struct.sizeof(blend.getEncoding().getAddressWidth()); + Constructor constructor = cStruct.getDeclaredConstructor(long.class, Block.class, BlockTable.class); + Object object = constructor.newInstance(address, b, blockTable); + + oStruct = (JSONObject) toJson(cStruct, object); + + } catch (Throwable e) { + oStruct = new JSONObject(); + e.printStackTrace(); + oStruct.put("error", e.getClass().getSimpleName() + ": " + e.getMessage()); + } + + return oStruct; + } + + private static Object toJson(Class type, Object value) throws Throwable { + try { + assert(value != null); + if (isPrimitive(type)) { + return codec.encodeObjectJSON(value); + } else if (value instanceof CArrayFacade){ + CArrayFacade carray = ((CArrayFacade)value); + JSONArray array = new JSONArray(); + boolean hasString = false; + for (int i = 0; i < carray.length(); i++) { + Object elem = carray.get(i); + if (elem instanceof Byte) { + hasString = true; + } + array.add(toJson(elem.getClass(), elem)); + } + if (hasString) { + JSONObject debugArray = new JSONObject(); + debugArray.put("str", carray.asString()); + debugArray.put("data", array); + return debugArray; + } else { + return array; + } + } else if (value instanceof CPointer) { + return ((CPointer)value).getAddress(); + } else if (value instanceof CFacade) { + JSONObject oStruct = new JSONObject(); + try { + for (Method getter : type.getDeclaredMethods()) { + if (getter.getName().startsWith("get")) { + Object result = getter.invoke(value); + Class rType = getter.getReturnType(); + oStruct.put(getter.getName(), toJson(rType, result)); + } + } + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof NullPointerException) { + throw (NullPointerException)cause; + } else { + throw e; + } + } + return oStruct; + } else { + return codec.encodeObjectJSON(value); + } + } catch (NullPointerException e) { + // caught when data from an address, that was assumed to + // be stored in the file, was not found. This happens in two cases: + // 1. The sdna index stored in the block header is not correct. In many + // cases index 0 (Link) is used for raw data. + // 2. A pointer references non-persistent data which was not stored in the file. + JSONObject o = new JSONObject(); + o.put("error", "data not found"); + return o; + } + } + + private static boolean isPrimitive(Class type) { + return type.isPrimitive() || type.equals(String.class); + } + +} diff --git a/src/main/java/com/replaymod/render/blend/Util.java b/src/main/java/com/replaymod/render/blend/Util.java new file mode 100644 index 00000000..f438f87e --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/Util.java @@ -0,0 +1,181 @@ +package com.replaymod.render.blend; + +import net.minecraft.client.renderer.GLAllocation; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.tileentity.TileEntity; +import org.blender.dna.Link; +import org.blender.dna.ListBase; +import org.blender.utils.BlenderFactory; +import org.cakelab.blender.nio.CPointer; +import org.lwjgl.opengl.GL11; +import org.lwjgl.util.vector.Matrix3f; +import org.lwjgl.util.vector.Matrix4f; +import org.lwjgl.util.vector.Quaternion; +import org.lwjgl.util.vector.Vector3f; + +import java.io.IOException; +import java.nio.FloatBuffer; + +public class Util { + public static BlenderFactory factory() { + return BlendState.getState().getFactory(); + } + + public static int align4(int size) { + return align(size, 4); + } + + public static int align(int size, int alignment) { + int misalignment = size % alignment; + if (misalignment > 0) { + return size + (alignment - misalignment); + } else { + return size; + } + } + + private static FloatBuffer floatBuffer = GLAllocation.createDirectFloatBuffer(16); + public static Matrix4f getGlMatrix(int matrix) { + floatBuffer.clear(); + GL11.glGetFloat(matrix, floatBuffer); + floatBuffer.rewind(); + Matrix4f mat = new Matrix4f(); + mat.load(floatBuffer); + return mat; + } + + public static Matrix4f getGlModelViewMatrix() { + return getGlMatrix(GL11.GL_MODELVIEW_MATRIX); + } + + public static Matrix4f getGlTextureMatrix() { + return getGlMatrix(GL11.GL_TEXTURE_MATRIX); + } + + public static boolean isGlTextureMatrixIdentity() { + Matrix4f mat = getGlTextureMatrix(); + // Assumes that glLoadIdentity uses positive zero floating point value (seems to be the case, at least on Mesa) + return mat.m00 == 1 && mat.m01 == 0 && mat.m02 == 0 && mat.m03 == 0 && + mat.m10 == 0 && mat.m11 == 1 && mat.m12 == 0 && mat.m13 == 0 && + mat.m20 == 0 && mat.m21 == 0 && mat.m22 == 1 && mat.m23 == 0 && + mat.m30 == 0 && mat.m31 == 0 && mat.m32 == 0 && mat.m33 == 1; + } + + public static Vector3f scaleFromMat(Matrix4f mat, Vector3f scale) { + if (scale == null) scale = new Vector3f(); + scale.set( + new Vector3f(mat.m00, mat.m01, mat.m02).length(), + new Vector3f(mat.m10, mat.m11, mat.m12).length(), + new Vector3f(mat.m20, mat.m21, mat.m22).length() + ); + Matrix3f m3 = new Matrix3f(); + m3.m00 = mat.m00; + m3.m01 = mat.m01; + m3.m02 = mat.m02; + m3.m10 = mat.m10; + m3.m11 = mat.m11; + m3.m12 = mat.m12; + m3.m20 = mat.m20; + m3.m21 = mat.m21; + m3.m22 = mat.m22; + if (m3.determinant() < 0) { + scale.x = -scale.x; + } + return scale; + } + + public static void scaleMat3(Matrix4f mat, Vector3f scale) { + mat.m00 /= scale.x; + mat.m01 /= scale.x; + mat.m02 /= scale.x; + mat.m10 /= scale.y; + mat.m11 /= scale.y; + mat.m12 /= scale.y; + mat.m20 /= scale.z; + mat.m21 /= scale.z; + mat.m22 /= scale.z; + } + + public static Vector3f posFromMat(Matrix4f mat, Vector3f pos) { + if (pos == null) pos = new Vector3f(); + pos.set(mat.m30, mat.m31, mat.m32); + return pos; + } + + public static Quaternion rotFromMat(Matrix4f mat, Quaternion rot) { + Quaternion result = Quaternion.setFromMatrix(mat, rot); + result.normalise(); + return result; + } + + public static void glToBlend(Vector3f vec) { + float tmp = vec.y; + vec.y = -vec.z; + vec.z = tmp; + } + + public static void glScaleToBlend(Vector3f vec) { + float tmp = vec.y; + vec.y = vec.z; + vec.z = tmp; + } + + public static void glToBlend(Quaternion q) { + float tmp = q.y; + q.y = -q.z; + q.z = tmp; + q.w = -q.w; + } + + public static Vector3f rotate(Quaternion rot, Vector3f vec, Vector3f dest) { + if (dest == null) dest = new Vector3f(); + Quaternion vecQ = new Quaternion(vec.x, vec.y, vec.z, 0); + Quaternion.mul(rot, vecQ, vecQ); + Quaternion.mulInverse(vecQ, rot, vecQ); + dest.set(vecQ); + return dest; + } + + // Original CPointer.plus method is broken, this one works with any non-negative numbers + public static CPointer plus(CPointer lhs, int rhs) throws IOException { + while (rhs > 0) { + lhs = lhs.plus(1); // 1 is the only value that can be passed to the original plus + rhs--; + } + return lhs; + } + + public static void insert(ListBase list, CPointer element) throws IOException { + CPointer oldFirst = list.getFirst().cast(Link.class); + if (oldFirst.isValid()) { + oldFirst.get().setPrev(element); + } + element.get().setNext(oldFirst); + list.setFirst(element.cast(Object.class)); + if (list.getLast().isNull()) { + list.setLast(element.cast(Object.class)); + } + } + + public static String getTileEntityId(TileEntity tileEntity) { + NBTTagCompound nbt = new NBTTagCompound(); + tileEntity.writeToNBT(nbt); + return nbt.getString("id"); + } + + public interface IOCallable { + R call() throws IOException; + } + + public interface IOConsumer { + void accept(T t) throws IOException; + } + + public interface IOBiConsumer { + void accept(T t, U u) throws IOException; + } + + public interface IOFunction { + R apply(T t) throws IOException; + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DAction.java b/src/main/java/com/replaymod/render/blend/data/DAction.java new file mode 100644 index 00000000..e99fddbc --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DAction.java @@ -0,0 +1,53 @@ +package com.replaymod.render.blend.data; + +import org.blender.dna.BezTriple; +import org.blender.dna.FCurve; +import org.blender.dna.bAction; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CArrayFacade; +import org.cakelab.blender.nio.CPointer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class DAction { + public final DId id = new DId(BlockCodes.ID_AC); + public List curves = new ArrayList<>(); + + public CPointer serialize(Serializer serializer) throws IOException { + return serializer.maybeMajor(this, id, bAction.class, () -> { + return bAction -> { + serializer.writeDataList(FCurve.class, bAction.getCurves(), curves.size(), (i, fCurve) -> { + DFCurve dfCurve = curves.get(i); + fCurve.setRna_path(serializer.writeString0(dfCurve.rnaPath)); + fCurve.setArray_index(dfCurve.rnaArrayIndex); + fCurve.setTotvert(dfCurve.keyframes.size()); + fCurve.setBezt(serializer.writeData(BezTriple.class, dfCurve.keyframes.size(), (j, fBezTriple) -> { + DKeyframe dKeyframe = dfCurve.keyframes.get(j); + fBezTriple.setIpo((byte) dKeyframe.interpolationType.ordinal()); + CArrayFacade vec = fBezTriple.getVec().get(1); + vec.set(0, (float) dKeyframe.frame); + vec.set(1, dKeyframe.value); + })); + }); + }; + }); + } + + public static class DFCurve { + public String rnaPath; + public int rnaArrayIndex; + public List keyframes = new ArrayList<>(); + } + + public static class DKeyframe { + public InterpolationType interpolationType = InterpolationType.CONSTANT; + public int frame; + public float value; + } + + public enum InterpolationType { + CONSTANT, LINEAR; + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DId.java b/src/main/java/com/replaymod/render/blend/data/DId.java new file mode 100644 index 00000000..287329d6 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DId.java @@ -0,0 +1,17 @@ +package com.replaymod.render.blend.data; + +import org.cakelab.blender.io.util.Identifier; + +public class DId { + public final Identifier code; + public String name; + + public DId(Identifier code) { + this.code = code; + } + + public DId(Identifier code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DImage.java b/src/main/java/com/replaymod/render/blend/data/DImage.java new file mode 100644 index 00000000..52b6f68b --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DImage.java @@ -0,0 +1,34 @@ +package com.replaymod.render.blend.data; + +import org.apache.commons.lang3.tuple.Pair; +import org.blender.dna.Image; +import org.blender.dna.ImagePackedFile; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CPointer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class DImage { + public final DId id = new DId(BlockCodes.ID_IM); + public String filePath; + public List> packedFiles = new ArrayList<>(); + + public CPointer serialize(Serializer serializer) throws IOException { + return serializer.maybeMajor(this, id, Image.class, () -> { + return image -> { + image.getName().fromString(String.valueOf(filePath)); + image.setSource((short) 1); + image.getColorspace_settings().getName().fromString("sRGB"); + image.setAspx(1); + image.setAspy(1); + serializer.writeDataList(ImagePackedFile.class, image.getPackedfiles(), packedFiles.size(), (i, pf) -> { + Pair pair = packedFiles.get(i); + pf.getFilepath().fromString(pair.getLeft()); + pf.setPackedfile(pair.getRight().serialize(serializer)); + }); + }; + }); + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DMaterial.java b/src/main/java/com/replaymod/render/blend/data/DMaterial.java new file mode 100644 index 00000000..3474ebef --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DMaterial.java @@ -0,0 +1,70 @@ +package com.replaymod.render.blend.data; + +import com.replaymod.render.blend.Util; +import org.blender.dna.MTex; +import org.blender.dna.Material; +import org.blender.dna.Tex; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CArrayFacade; +import org.cakelab.blender.nio.CPointer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class DMaterial { + public final DId id = new DId(BlockCodes.ID_MA); + public List textures = new ArrayList<>(); + + public CPointer serialize(Serializer serializer) throws IOException { + return serializer.maybeMajor(this, id, Material.class, () -> { + List>> textures = new ArrayList<>(); + for (DMTex texture : this.textures) { + textures.add(texture.serialize(serializer)); + } + return material -> { + material.setMode( + 0x10000 /* MA_TRANSP */ | 0x40 /* MA_ZTRANS */ | 0x80 /* MA_VERTEXCOLP */ + ); + material.setAlpha(1); + material.setRef(1); + material.setR(1); + material.setG(1); + material.setB(1); + CArrayFacade> mTexPointers = material.getMtex(); + for (int i = 0; i < textures.size(); i++) { + mTexPointers.set(i, textures.get(i).call()); + } + }; + }); + } + + public static class DMTex { + public DTexture texture; + + public Util.IOCallable> serialize(Serializer serializer) throws IOException { + CPointer texture = this.texture.serialize(serializer); + return () -> { + MTex mTex = serializer.writeData(MTex.class); + mTex.setTex(texture); + mTex.getUvname().fromString("UVMap"); + mTex.setMapto((short) ( + 0x1 /* MAP_COL */ | 0x80 /* MAP_ALPHA */ + )); + mTex.setBlendtype((short) 0x1 /* MTEX_MUL */); + mTex.setColfac(1); + mTex.setAlphafac(1); + mTex.setDef_var(1); + mTex.setTexco((short) 16); + mTex.setProjx((byte) 1); + mTex.setProjy((byte) 2); + mTex.setProjz((byte) 3); + CArrayFacade size = mTex.getSize(); + size.set(0, 1f); + size.set(1, 1f); + size.set(2, 1f); + return mTex.__io__addressof(); + }; + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DMesh.java b/src/main/java/com/replaymod/render/blend/data/DMesh.java new file mode 100644 index 00000000..70aaf174 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DMesh.java @@ -0,0 +1,295 @@ +package com.replaymod.render.blend.data; + +import net.minecraft.util.Vec3i; +import org.blender.dna.CustomData; +import org.blender.dna.CustomDataLayer; +import org.blender.dna.Image; +import org.blender.dna.MEdge; +import org.blender.dna.MFace; +import org.blender.dna.MLoop; +import org.blender.dna.MLoopCol; +import org.blender.dna.MLoopUV; +import org.blender.dna.MPoly; +import org.blender.dna.MTexPoly; +import org.blender.dna.MVert; +import org.blender.dna.Material; +import org.blender.dna.Mesh; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CArrayFacade; +import org.cakelab.blender.nio.CPointer; +import org.lwjgl.util.vector.Vector2f; +import org.lwjgl.util.vector.Vector3f; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class DMesh { + public final DId id = new DId(BlockCodes.ID_ME); + public final List vertices = new ArrayList<>(); + public final List edges = new ArrayList<>(); + public final List faces = new ArrayList<>(); + public final List loops = new ArrayList<>(); + public final List polys = new ArrayList<>(); + public final List materials = new ArrayList<>(); + + public CPointer serialize(Serializer serializer) throws IOException { + return serializer.maybeMajor(this, id, Mesh.class, () -> { + List> materials = new ArrayList<>(); + List> images = new ArrayList<>(); + for (DMaterial material : this.materials) { + materials.add(material.serialize(serializer)); + images.add(material.textures.isEmpty() ? null : material.textures.get(0).texture.image.serialize(serializer)); + } + return mesh -> { + mesh.setDrawflag(3 /* Edges & Faces */); + CArrayFacade> mat = serializer.writeDataPArray(Material.class, materials.size(), materials::get); + mesh.setMat(mat); + mesh.setTotcol((short) materials.size()); + + if (!vertices.isEmpty()) { + CArrayFacade mVerts = serializer.writeData(MVert.class, vertices.size(), + (i, mVert) -> vertices.get(i).serialize(mVert)); + + CustomDataLayer vDataLayer = serializer.writeData(CustomDataLayer.class); + vDataLayer.setType(0 /* CD_MVERT */); + vDataLayer.setData(mVerts.cast(Object.class)); + + CustomData vData = mesh.getVdata(); + vData.setMaxlayer(1); + vData.setTotlayer(1); + vData.setLayers(vDataLayer.__io__addressof()); + + mesh.setMvert(mVerts); + mesh.setTotvert(vertices.size()); + } + if (!edges.isEmpty()) { + CArrayFacade mEdges = serializer.writeData(MEdge.class, edges.size(), + (i, mEdge) -> edges.get(i).serialize(mEdge)); + + CustomDataLayer eDataLayer = serializer.writeData(CustomDataLayer.class); + eDataLayer.setType(3 /* CD_MEDGE */); + eDataLayer.setData(mEdges.cast(Object.class)); + + CustomData eData = mesh.getEdata(); + eData.setMaxlayer(1); + eData.setTotlayer(1); + eData.setLayers(eDataLayer.__io__addressof()); + + mesh.setMedge(mEdges); + mesh.setTotedge(edges.size()); + + + mesh.setMface(serializer.writeData(MFace.class, faces.size(), + (i, mFace) -> faces.get(i).serialize(mFace))); + mesh.setTotface(faces.size()); + } + if (!loops.isEmpty()) { + CArrayFacade mLoops = serializer.writeData(MLoop.class, loops.size(), + (i, mLoop) -> loops.get(i).serialize(mLoop)); + CArrayFacade mLoopUVs = serializer.writeData(MLoopUV.class, loops.size(), + (i, mLoopUV) -> loops.get(i).serialize(mLoopUV)); + CArrayFacade mLoopCols = serializer.writeData(MLoopCol.class, loops.size(), + (i, mLoopCol) -> loops.get(i).serialize(mLoopCol)); + + CArrayFacade dataLayers = serializer.writeData(CustomDataLayer.class, 3); + int i = 0; + + CustomDataLayer luvDataLayer = dataLayers.get(i++); + luvDataLayer.setType(16 /* CD_MLOOPUV */); + luvDataLayer.setData(mLoopUVs.cast(Object.class)); + + CustomDataLayer lcolDataLayer = dataLayers.get(i++); + lcolDataLayer.setType(17 /* CD_MLOOPCOL */); + lcolDataLayer.setData(mLoopCols.cast(Object.class)); + + CustomDataLayer lDataLayer = dataLayers.get(i++); + lDataLayer.setType(26 /* CD_MLOOP */); + lDataLayer.setData(mLoops.cast(Object.class)); + + CustomData lData = mesh.getLdata(); + lData.setMaxlayer(i); + lData.setTotlayer(i); + lData.setLayers(dataLayers); + + mesh.setMloop(mLoops); + mesh.setMloopuv(mLoopUVs); + mesh.setTotloop(loops.size()); + } + if (!polys.isEmpty()) { + CArrayFacade mPolys = serializer.writeData(MPoly.class, polys.size(), + (i, mPoly) -> polys.get(i).serialize(mPoly)); + CArrayFacade mTexPolys = serializer.writeData(MTexPoly.class, polys.size(), + (i, mTexPoly) -> mTexPoly.setTpage(images.get(polys.get(i).materialSlot))); + + CArrayFacade dataLayers = serializer.writeData(CustomDataLayer.class, mTexPolys != null ? 2 : 1); + int i = 0; + + if (mTexPolys != null) { + CustomDataLayer pTexDataLayer = dataLayers.get(i++); + pTexDataLayer.getName().fromString("UVMap"); + pTexDataLayer.setType(15 /* CD_MTEXPOLY */); + pTexDataLayer.setData(mTexPolys.cast(Object.class)); + } + + CustomDataLayer pDataLayer = dataLayers.get(i++); + pDataLayer.setType(25 /* CD_MPOLY */); + pDataLayer.setData(mPolys.cast(Object.class)); + + CustomData pData = mesh.getPdata(); + pData.setMaxlayer(i); + pData.setTotlayer(i); + pData.setLayers(dataLayers); + + mesh.setMpoly(mPolys); + mesh.setMtpoly(mTexPolys); + mesh.setTotpoly(polys.size()); + } + }; + }); + } + + public void addTriangle(DMesh.Vertex v1, DMesh.Vertex v2, DMesh.Vertex v3, + Vector2f uv1, Vector2f uv2, Vector2f uv3, + int c1, int c2, int c3, + int materialSlot) { + int vOffset = vertices.size(); + int eOffset = edges.size(); + int lOffset = loops.size(); + vertices.add(v1); + vertices.add(v2); + vertices.add(v3); + edges.add(new Edge(vOffset , vOffset + 1)); + edges.add(new Edge(vOffset + 1, vOffset + 2)); + edges.add(new Edge(vOffset + 2, vOffset )); + loops.add(new Loop(vOffset , eOffset , uv1.x, uv1.y, c1)); + loops.add(new Loop(vOffset + 1, eOffset + 1, uv2.x, uv2.y, c2)); + loops.add(new Loop(vOffset + 2, eOffset + 2, uv3.x, uv3.y, c3)); + polys.add(new Poly(lOffset, 3, materialSlot)); + } + + public void addQuad(DMesh.Vertex v1, DMesh.Vertex v2, DMesh.Vertex v3, DMesh.Vertex v4, + Vector2f uv1, Vector2f uv2, Vector2f uv3, Vector2f uv4, + int c1, int c2, int c3, int c4, + int materialSlot) { + int vOffset = vertices.size(); + int eOffset = edges.size(); + int lOffset = loops.size(); + vertices.add(v1); + vertices.add(v2); + vertices.add(v3); + vertices.add(v4); + edges.add(new Edge(vOffset , vOffset + 1)); + edges.add(new Edge(vOffset + 1, vOffset + 2)); + edges.add(new Edge(vOffset + 2, vOffset + 3)); + edges.add(new Edge(vOffset + 3, vOffset )); + loops.add(new Loop(vOffset , eOffset , uv1.x, uv1.y, c1)); + loops.add(new Loop(vOffset + 1, eOffset + 1, uv2.x, uv2.y, c2)); + loops.add(new Loop(vOffset + 2, eOffset + 2, uv3.x, uv3.y, c3)); + loops.add(new Loop(vOffset + 3, eOffset + 3, uv4.x, uv4.y, c4)); + polys.add(new Poly(lOffset, 4, materialSlot)); + } + + public static class Vertex { + public Vector3f pos; + public Vec3i norm = Vec3i.NULL_VECTOR; + + public Vertex(float x, float y, float z) { + this.pos = new Vector3f(x, y, z); + } + + public void serialize(MVert mVert) throws IOException { + CArrayFacade pos = mVert.getCo(); + pos.set(0, this.pos.x); + pos.set(1, this.pos.y); + pos.set(2, this.pos.z); + CArrayFacade norm = mVert.getNo(); + norm.set(0, (short) this.norm.getX()); + norm.set(1, (short) this.norm.getY()); + norm.set(2, (short) this.norm.getZ()); + } + } + + public static class Edge { + public int v1, v2; + + public Edge(int v1, int v2) { + this.v1 = v1; + this.v2 = v2; + } + + public void serialize(MEdge mEdge) throws IOException { + mEdge.setV1(v1); + mEdge.setV2(v2); + } + } + + public static class Loop { + public int vertex, edge; + public float u, v; + public int col; + + public Loop(int vertex, int edge, + float u, float v, + int col) { + this.vertex = vertex; + this.edge = edge; + this.u = u; + this.v = v; + this.col = col; + } + + public void serialize(MLoop mLoop) throws IOException { + mLoop.setV(vertex); + mLoop.setE(edge); + } + + public void serialize(MLoopUV mLoop) throws IOException { + CArrayFacade uv = mLoop.getUv(); + uv.set(0, u); + uv.set(1, v); + } + + public void serialize(MLoopCol mLoop) throws IOException { + mLoop.setR((byte) ((col ) & 0xff)); + mLoop.setG((byte) ((col >> 8) & 0xff)); + mLoop.setB((byte) ((col >> 16) & 0xff)); + mLoop.setA((byte) ((col >> 24) & 0xff)); + } + } + + public static class Poly { + public int loopStart, size; + public short materialSlot; + + public Poly(int loopStart, int size, int materialSlot) { + this.loopStart = loopStart; + this.size = size; + this.materialSlot = (short) materialSlot; + } + + public void serialize(MPoly mPoly) throws IOException { + mPoly.setLoopstart(loopStart); + mPoly.setTotloop(size); + mPoly.setMat_nr(materialSlot); + } + } + + public static class Face { + public int v1, v2, v3, v4; + + public Face(int v1, int v2, int v3, int v4) { + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.v4 = v4; + } + + public void serialize(MFace mFace) throws IOException { + mFace.setV1(v1); + mFace.setV2(v2); + mFace.setV3(v3); + mFace.setV4(v4); + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DObject.java b/src/main/java/com/replaymod/render/blend/data/DObject.java new file mode 100644 index 00000000..2ea96831 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DObject.java @@ -0,0 +1,270 @@ +package com.replaymod.render.blend.data; + +import com.replaymod.render.blend.Util; +import org.blender.dna.AnimData; +import org.blender.dna.BlenderObject; +import org.blender.dna.Material; +import org.blender.dna.Mesh; +import org.blender.dna.bAction; +import org.blender.dna.bConstraint; +import org.blender.dna.bTrackToConstraint; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CArrayFacade; +import org.cakelab.blender.nio.CPointer; +import org.lwjgl.util.vector.Quaternion; +import org.lwjgl.util.vector.Vector3f; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class DObject { + public final DId id = new DId(BlockCodes.ID_OB); + public Vector3f loc = new Vector3f(0, 0, 0); + public Vector3f scale = new Vector3f(1, 1, 1); + public Quaternion rot = new Quaternion(); + private DObject parent; + private List children = new ArrayList<>(); + private List unmodifiableChildren; + public DAction action; + public DMesh mesh; + public Type type; + public int layers = 1; + public DObject pointAt; + public int lastFrame; + private int lastVisibleFrame = -2; + + public DObject(Type type) { + this.type = type; + } + + public DObject(DMesh mesh) { + this(Type.OB_MESH); + this.mesh = mesh; + } + + public boolean isValid() { + return true; + } + + public void setVisible(int frame) { + if (frame < lastVisibleFrame) throw new IllegalStateException("Already at frame " + lastVisibleFrame); + if (lastVisibleFrame < frame - 1) { + if (lastVisibleFrame < 0) { + if (frame > 0) { + keyframe("hide", 0, 0, 1); + } + } else { + keyframe("hide", 0, lastVisibleFrame + 1, 1); + } + keyframe("hide", 0, frame, 0); + } + lastVisibleFrame = frame; + } + + public void setParent(DObject parent) { + if (this.parent != null) { + this.parent.children.remove(this); + } + this.parent = parent; + parent.children.add(this); + } + + public DObject getParent() { + return parent; + } + + public List getChildren() { + if (unmodifiableChildren == null) { + unmodifiableChildren = Collections.unmodifiableList(children); + } + return unmodifiableChildren; + } + + public void keyframeLocRotScale(int frame) { + keyframeLoc(frame); + keyframeRot(frame); + keyframeScale(frame); + } + + public void keyframeLoc(int frame) { + keyframe("location", frame, loc); + } + + public void keyframeRot(int frame) { + keyframe("rotation_quaternion", frame, rot); + } + + public void keyframeScale(int frame) { + keyframe("scale", frame, scale); + } + + public void keyframe(String rnaPath, int frame, Quaternion q) { + keyframe(rnaPath, 0, frame, q.w); + keyframe(rnaPath, 1, frame, q.x); + keyframe(rnaPath, 2, frame, q.y); + keyframe(rnaPath, 3, frame, q.z); + } + + public void keyframe(String rnaPath, int frame, Vector3f vec) { + keyframe(rnaPath, 0, frame, vec.x); + keyframe(rnaPath, 1, frame, vec.y); + keyframe(rnaPath, 2, frame, vec.z); + } + + public void keyframe(String rnaPath, int rnaArrayIndex, int frame, float value) { + if (frame < 0) return; + DAction.DKeyframe keyframe = new DAction.DKeyframe(); + keyframe.frame = frame; + keyframe.value = value; + keyframe(rnaPath, rnaArrayIndex, keyframe, rnaPath.startsWith("hide")); + if (rnaPath.equals("hide")) { + keyframe("hide_render", rnaArrayIndex, frame, value); + } + } + + public void keyframe(String rnaPath, int rnaArrayIndex, DAction.DKeyframe keyframe, boolean constant) { + if (action == null) { + action = new DAction(); + } + DAction.DFCurve theCurve = null; + for (DAction.DFCurve curve : action.curves) { + if (curve.rnaArrayIndex == rnaArrayIndex && curve.rnaPath.equals(rnaPath)) { + theCurve = curve; + break; + } + } + if (theCurve == null) { + theCurve = new DAction.DFCurve(); + theCurve.rnaPath = rnaPath; + theCurve.rnaArrayIndex = rnaArrayIndex; + action.curves.add(theCurve); + } + keyframe.interpolationType = constant ? DAction.InterpolationType.CONSTANT : DAction.InterpolationType.LINEAR; + if (constant) { + if (!theCurve.keyframes.isEmpty()) { + DAction.DKeyframe prev = theCurve.keyframes.get(theCurve.keyframes.size() - 1); + if (Math.abs(prev.value - keyframe.value) < 1e-4) { + return; + } + } + } else { + if (theCurve.keyframes.size() >= 2) { + DAction.DKeyframe prev = theCurve.keyframes.get(theCurve.keyframes.size() - 1); + DAction.DKeyframe prev2 = theCurve.keyframes.get(theCurve.keyframes.size() - 2); + float m = (prev.value - prev2.value) / (prev.frame - prev2.frame); + float interpolatedValue = prev.value + m * (keyframe.frame - prev.frame); + if (Math.abs(interpolatedValue - keyframe.value) < 1e-4) { + theCurve.keyframes.remove(theCurve.keyframes.size() - 1); + } + } + } + theCurve.keyframes.add(keyframe); + } + + public CPointer serialize(Serializer serializer) throws IOException { + return serializer.maybeMajor(this, id, BlenderObject.class, () -> { + if (lastVisibleFrame >= 0) { + keyframe("hide", 0, lastVisibleFrame + 1, 1); + } + + CPointer parent = this.parent == null ? null : this.parent.serialize(serializer); + CPointer mesh = this.mesh == null ? null : this.mesh.serialize(serializer); + CPointer action = this.action == null ? null : this.action.serialize(serializer); + CPointer pointAt = this.pointAt == null ? null : this.pointAt.serialize(serializer); + + return object -> { + object.setParent(parent); + if (parent != null) { + CArrayFacade> parentinv = object.getParentinv(); + parentinv.get(0).set(0, 1f); + parentinv.get(1).set(1, 1f); + parentinv.get(2).set(2, 1f); + parentinv.get(3).set(3, 1f); + } + if (mesh != null) { + object.setData(mesh.cast(Object.class)); + Mesh meshObj = mesh.get(); + short totcol = meshObj.getTotcol(); + if (totcol > 0) { + byte[] matbits = new byte[totcol]; + Arrays.fill(matbits, (byte) 1); + object.setMatbits(serializer.writeBytes(matbits)); + object.setMat(serializer.writeDataPArray(Material.class, totcol, i -> Util.plus(meshObj.getMat(), i).get())); + object.setTotcol(totcol); + } + } + object.setType((short) type.ordinal()); + object.setLay(layers); + object.setDt((byte) 5); + CArrayFacade loc = object.getLoc(); + loc.set(0, this.loc.x); + loc.set(1, this.loc.y); + loc.set(2, this.loc.z); + CArrayFacade size = object.getSize(); + size.set(0, scale.x); + size.set(1, scale.y); + size.set(2, scale.z); + CArrayFacade quat = object.getQuat(); + quat.set(0, rot.w); + quat.set(1, rot.x); + quat.set(2, rot.y); + quat.set(3, rot.z); + CArrayFacade dquat = object.getDquat(); + dquat.set(0, 1f); + dquat.set(1, 0f); + dquat.set(2, 0f); + dquat.set(3, 0f); + + object.getDscale().fromArray(new float[]{1, 1, 1}); + + AnimData animData = serializer.writeData(AnimData.class); + animData.setAction(action); + object.setAdt(animData.__io__addressof()); + + if (pointAt != null) { + serializer.writeDataList(bConstraint.class, object.getConstraints(), 1, (i, bConstraint) -> { + bConstraint.setEnforce(1); + bConstraint.setType((short) 2 /* CONSTRAINT_TYPE_TRACKTO */); + bTrackToConstraint constraint = serializer.writeData(bTrackToConstraint.class); + constraint.setTar(pointAt); + constraint.setReserved1(1 /* TRACK_Y */); + constraint.setReserved2(2 /* UP_Z */); + bConstraint.setData(constraint.__io__addressof().cast(Object.class)); + }); + } + }; + }); + } + + public enum Type { + OB_EMPTY, + OB_MESH, + OB_CURVE, + OB_SURF, + OB_FONT, + OB_MBALL, + OB_6, + OB_7, + OB_8, + OB_9, + OB_LAMP, + OB_CAMERA, + OB_SPEAKER, + OB_13, + OB_14, + OB_15, + OB_16, + OB_17, + OB_18, + OB_19, + OB_20, + OB_WAVE, + OB_LATTICE, + OB_23, + OB_24, + OB_ARMATURE + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DPackedFile.java b/src/main/java/com/replaymod/render/blend/data/DPackedFile.java new file mode 100644 index 00000000..a5460827 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DPackedFile.java @@ -0,0 +1,24 @@ +package com.replaymod.render.blend.data; + +import org.blender.dna.PackedFile; +import org.cakelab.blender.nio.CPointer; + +import java.io.IOException; + +public class DPackedFile { + public byte[] data; + + public DPackedFile() {} + + public DPackedFile(byte[] data) { + this.data = data; + } + + public CPointer serialize(Serializer serializer) throws IOException { + PackedFile packedFile = serializer.writeData(PackedFile.class); + packedFile.setSize(data.length); + packedFile.setSeek(0); + packedFile.setData(serializer.writeBytes(data).cast(Object.class)); + return packedFile.__io__addressof(); + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DScene.java b/src/main/java/com/replaymod/render/blend/data/DScene.java new file mode 100644 index 00000000..ad5d8e23 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DScene.java @@ -0,0 +1,64 @@ +package com.replaymod.render.blend.data; + +import org.blender.dna.Base; +import org.blender.dna.BlenderObject; +import org.blender.dna.RenderData; +import org.blender.dna.Scene; +import org.blender.dna.ToolSettings; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CPointer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class DScene { + public final DId id = new DId(BlockCodes.ID_SCE, "Scene"); + public final List base = new ArrayList<>(); + public int endFrame; + + private Set findAllObjects(DObject curr, Set set) { + for (DObject child : curr == null ? base : curr.getChildren()) { + if (child.isValid()) { + findAllObjects(child, set); + set.add(child); + } + } + return set; + } + + public CPointer serialize(Serializer serializer) throws IOException { + return serializer.maybeMajor(this, id, Scene.class, () -> { + Set allObjects = findAllObjects(null, new LinkedHashSet<>()); + List> bases = new ArrayList<>(allObjects.size()); + for (DObject dObject : allObjects) { + bases.add(dObject.serialize(serializer)); + } + return scene -> { + serializer.writeDataList(Base.class, scene.getBase(), bases.size(), (i, base) -> { + CPointer objPointer = bases.get(i); + base.setLay(objPointer.get().getLay()); + base.setObject(objPointer); + }); + + RenderData renderData = scene.getR(); + renderData.setXsch(1920); + renderData.setYsch(1080); + renderData.setSize((short) 100); + renderData.setXasp(1); + renderData.setYasp(1); + renderData.setTilex(64); + renderData.setTiley(64); + renderData.setEfra(endFrame); + renderData.setFrame_step(1); + renderData.setFramapto(100); + renderData.setImages(100); + + ToolSettings toolSettings = serializer.writeData(ToolSettings.class); + scene.setToolsettings(toolSettings.__io__addressof()); + }; + }); + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/DTexture.java b/src/main/java/com/replaymod/render/blend/data/DTexture.java new file mode 100644 index 00000000..a7273627 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DTexture.java @@ -0,0 +1,32 @@ +package com.replaymod.render.blend.data; + +import org.blender.dna.Image; +import org.blender.dna.Tex; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CPointer; + +import java.io.IOException; + +public class DTexture { + public final DId id = new DId(BlockCodes.ID_TE); + public DImage image; + + public CPointer serialize(Serializer serializer) throws IOException { + return serializer.maybeMajor(this, id, Tex.class, () -> { + CPointer image = this.image.serialize(serializer); + return tex -> { + tex.setIma(image); + tex.setType((short) 8 /* TEX_IMAGE */); + tex.setImaflag((short) 2 /* TEX_USEALPHA */); + tex.setCropxmax(1); + tex.setCropymax(1); + tex.setRfac(1); + tex.setGfac(1); + tex.setBfac(1); + tex.setBright(1); + tex.setContrast(1); + tex.setSaturation(1); + }; + }); + } +} diff --git a/src/main/java/com/replaymod/render/blend/data/Serializer.java b/src/main/java/com/replaymod/render/blend/data/Serializer.java new file mode 100644 index 00000000..63b28a50 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/Serializer.java @@ -0,0 +1,137 @@ +package com.replaymod.render.blend.data; + +import com.replaymod.render.blend.Util; +import com.replaymod.render.blend.Util.IOBiConsumer; +import com.replaymod.render.blend.Util.IOCallable; +import com.replaymod.render.blend.Util.IOConsumer; +import org.blender.dna.ID; +import org.blender.dna.Link; +import org.blender.dna.ListBase; +import org.cakelab.blender.io.block.BlockCodes; +import org.cakelab.blender.nio.CArrayFacade; +import org.cakelab.blender.nio.CFacade; +import org.cakelab.blender.nio.CPointer; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static com.replaymod.render.blend.Util.align4; +import static com.replaymod.render.blend.Util.factory; +import static org.cakelab.blender.io.block.BlockCodes.ID_DATA; + +public class Serializer { + private final Map> serialized = new HashMap<>(); + private final Set usedNames = new HashSet<>(); + + public CPointer getMajor(Object obj, Class clazz) { + CPointer pointer = serialized.get(obj); + if (pointer == null) { + return null; + } + return pointer.cast(clazz); + } + + public CPointer writeMajor(Object obj, DId id, Class clazz) throws IOException { + if (serialized.containsKey(obj)) { + throw new IllegalStateException("Object " + obj + " already serialized."); + } + + T val = factory().newCStructBlock(id.code, clazz); + CPointer pointer = CFacade.__io__addressof(val); + + serialized.put(obj, pointer.cast(Object.class)); + + if (id.code != ID_DATA) { + ID asID = pointer.cast(ID.class).get(); + String name = id.code.toString().substring(0, 2) + id.name; + String fullName = name; + int i = 0; + while (usedNames.contains(fullName)) { + fullName = name + "." + i; + i++; + } + usedNames.add(fullName); + asID.getName().fromString(fullName); + } + + return pointer; + } + + public CPointer maybeMajor(Object obj, DId id, Class clazz, IOCallable> prepare) throws IOException { + CPointer result = getMajor(obj, clazz); + if (result == null) { + IOConsumer configure = prepare.call(); + result = writeMajor(obj, id, clazz); + configure.accept(result.get()); + } + return result; + } + + public CArrayFacade writeString0(String str) throws IOException { + byte[] bytes = (str + '\0').getBytes(); + CArrayFacade pointer = factory().newCArrayBlock(BlockCodes.ID_DATA, Byte.class, align4(bytes.length)); + pointer.fromArray(bytes); + return pointer; + } + + public CPointer writeBytes(byte[] bytes) throws IOException { + CArrayFacade pointer = factory().newCArrayBlock(BlockCodes.ID_DATA, Byte.class, align4(bytes.length)); + pointer.fromArray(bytes); + return pointer; + } + + public T writeData(Class clazz) throws IOException { + return factory().newCStructBlock(ID_DATA, clazz); + } + + public CArrayFacade writeData(Class clazz, int count) throws IOException { + return factory().newCStructBlock(ID_DATA, clazz, count); + } + + public CArrayFacade writeData(Class clazz, int count, IOBiConsumer forElem) throws IOException { + if (count == 0) return null; + CArrayFacade arrayFacade = writeData(clazz, count); + CPointer pointer = arrayFacade; + for (int i = 0; i < count; i++) { + forElem.accept(i, pointer.get()); + pointer = Util.plus(pointer, 1); + } + return arrayFacade; + } + + public CArrayFacade> writeDataPArray(Class clazz, int count, Util.IOFunction> forElem) throws IOException { + if (count == 0) return null; + CArrayFacade> arrayFacade = factory().newCPointerBlock(ID_DATA, new Class[]{CPointer.class, clazz}, count); + for (int i = 0; i < count; i++) { + arrayFacade.set(i, forElem.apply(i)); + } + return arrayFacade; + } + + public void writeDataList(Class clazz, ListBase listBase, int size, IOBiConsumer forElem) throws IOException { + CPointer prevPointer = null; + Link prev = null; + for (int i = 0; i < size; i++) { + CPointer pointer = CFacade.__io__addressof(writeData(clazz)); + CPointer linkPointer = pointer.cast(Link.class); + Link linkElem = linkPointer.get(); + if (prevPointer == null) { + listBase.setFirst(pointer.cast(Object.class)); + } else { + prev.setNext(linkPointer); + linkElem.setPrev(prevPointer); + } + + forElem.accept(i, pointer.get()); + + prevPointer = linkPointer; + prev = linkElem; + } + if (prevPointer != null) { + listBase.setLast(prevPointer.cast(Object.class)); + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/exporters/ChunkExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ChunkExporter.java new file mode 100644 index 00000000..c6f28601 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ChunkExporter.java @@ -0,0 +1,185 @@ +package com.replaymod.render.blend.exporters; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DMaterial; +import com.replaymod.render.blend.data.DMesh; +import com.replaymod.render.blend.data.DObject; +import com.replaymod.replaystudio.us.myles.ViaVersion.util.ReflectionUtil; +import lombok.SneakyThrows; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GLAllocation; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.chunk.CompiledChunk; +import net.minecraft.client.renderer.chunk.RenderChunk; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.renderer.vertex.VertexBuffer; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumWorldBlockLayer; +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.opengl.GL15; +import org.lwjgl.util.vector.Vector2f; +import org.lwjgl.util.vector.Vector3f; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ChunkExporter implements Exporter { + private final Map chunkObjects = new HashMap<>(); + private final Map> chunkLayerObjects = new HashMap<>(); + private final List> chunks = new ArrayList<>(); + private DObject chunksObject; + private DMaterial material; + private int frame; + + public void addChunkUpdate(RenderChunk chunk, EnumWorldBlockLayer layer) { + chunks.add(Pair.of(chunk, layer)); + } + + @Override + @SneakyThrows + public void setup() throws IOException { + + Minecraft mc = Minecraft.getMinecraft(); + @SuppressWarnings("unchecked") + List renderInfos = mc.renderGlobal.renderInfos; + for (RenderGlobal.ContainerLocalRenderInformation renderInfo : renderInfos) { + RenderChunk renderChunk = ReflectionUtil.get(renderInfo, "renderChunk", RenderChunk.class); // SneakyThrows + CompiledChunk compiledChunk = renderChunk.getCompiledChunk(); + if (!compiledChunk.isEmpty()) { + for (EnumWorldBlockLayer layer : EnumWorldBlockLayer.values()) { + addChunkUpdate(renderChunk, layer); + } + } + } + + chunksObject = new DObject(DObject.Type.OB_EMPTY); + chunksObject.id.name = "Chunks"; + chunksObject.layers = 1 << 1; + BlendState.getState().getScene().base.add(chunksObject); + } + + @Override + public void tearDown() throws IOException { + chunks.clear(); + chunkObjects.clear(); + chunksObject = null; + } + + @Override + public void preFrame(int frame) throws IOException { + this.frame = frame; + } + + @Override + public void postFrame(int frame) throws IOException { + Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.locationBlocksTexture); + material = BlendState.getState().getMaterials().getActiveMaterial(); + + for (Pair pair : chunks) { + RenderChunk chunk = pair.getLeft(); + EnumWorldBlockLayer layer = pair.getRight(); + DObject chunkObject = chunkObjects.get(chunk.getPosition()); + if (chunkObject == null) { + chunkObject = buildChunkObject(chunk); + chunkObjects.put(chunk.getPosition(), chunkObject); + chunkLayerObjects.put(chunk.getPosition(), new EnumMap<>(EnumWorldBlockLayer.class)); + } + DObject layerObject = buildChunkLayerObject(chunkObject, chunk, layer); + if (layerObject == null) continue; + Map layerObjects = chunkLayerObjects.get(chunk.getPosition()); + DObject oldLayerObject = layerObjects.get(layer); + if (oldLayerObject != null) { + oldLayerObject.keyframe("hide", 0, frame, 1f); + } + if (frame > 0) { + layerObject.keyframe("hide", 0, frame - 1, 1f); + layerObject.keyframe("hide", 0, frame, 0f); + } else { + layerObject.keyframe("hide", 0, frame, 0f); + } + layerObjects.put(layer, layerObject); + } + chunks.clear(); + } + + private DObject buildChunkObject(RenderChunk renderChunk) { + BlockPos pos = renderChunk.getPosition(); + DObject chunkObject = new DObject(DObject.Type.OB_EMPTY); + chunkObject.setParent(chunksObject); + chunkObject.id.name = "Chunk[" + pos.getX()/16 + "/" + pos.getY()/16 + "/" + pos.getZ()/16 + "]"; + chunkObject.loc = new Vector3f(pos.getX(), -pos.getZ(), pos.getY()); + return chunkObject; + } + + private DObject buildChunkLayerObject(DObject chunkObject, RenderChunk renderChunk, EnumWorldBlockLayer layer) { + VertexBuffer vertexBuffer = renderChunk.getVertexBufferByLayer(layer.ordinal()); + if (vertexBuffer == null) return null; + + DObject layerObject = new DObject(buildChunkLayerMesh(vertexBuffer)); + layerObject.setParent(chunkObject); + layerObject.id.name = chunkObject.id.name + " - " + layer + " - Frame " + frame; + return layerObject; + } + + private DMesh buildChunkLayerMesh(VertexBuffer vertexBuffer) { + vertexBuffer.bindBuffer(); + int size = GL15.glGetBufferParameteri(OpenGlHelper.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE); + ByteBuffer byteBuffer = GLAllocation.createDirectByteBuffer(size); + GL15.glGetBufferSubData(OpenGlHelper.GL_ARRAY_BUFFER, 0, byteBuffer); + vertexBuffer.unbindBuffer(); + + DMesh mesh = new DMesh(); + mesh.materials.add(material); + DMesh.Vertex v1 = null, v2 = null, v3 = null; + Vector2f uv1 = new Vector2f(), + uv2 = new Vector2f(), + uv3 = new Vector2f(), + uv4 = new Vector2f(); + int c1 = 0, c2 = 0, c3 = 0; + for (int i = 0; i < size / STRIDE; i++) { + // See VboRenderList.setupArrayPointers for offsets + DMesh.Vertex vert = new DMesh.Vertex( + byteBuffer.getFloat(STRIDE * i), + -byteBuffer.getFloat(STRIDE * i + 8), + byteBuffer.getFloat(STRIDE * i + 4) + ); + float u = byteBuffer.getFloat(STRIDE * i + 16); + float v = 1 - byteBuffer.getFloat(STRIDE * i + 20); + int c = byteBuffer.getInt(STRIDE * i + 12); + // TODO tex, norm, etc. + if (v1 == null) { + v1 = vert; + uv1.set(u, v); + c1 = c; + } else if (v2 == null) { + v2 = vert; + uv2.set(u, v); + c2 = c; + } else if (v3 == null) { + v3 = vert; + uv3.set(u, v); + c3 = c; + } else { + uv4.set(u, v); + mesh.addQuad( + v1, v2, v3, vert, + uv1, uv2, uv3, uv4, + c1, c2, c3, c, + 0 + ); + v1 = v2 = v3 = null; + } + } + + return mesh; + } + + private static final int STRIDE = 28; +} diff --git a/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java b/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java new file mode 100644 index 00000000..1fa342e8 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java @@ -0,0 +1,83 @@ +package com.replaymod.render.blend.exporters; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DObject; +import lombok.SneakyThrows; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.Entity; +import org.lwjgl.util.vector.Matrix4f; +import org.lwjgl.util.vector.Vector3f; + +import java.io.IOException; +import java.util.IdentityHashMap; +import java.util.Map; + +import static com.replaymod.render.blend.Util.getGlModelViewMatrix; + +public class EntityExporter implements Exporter { + private final Minecraft mc = Minecraft.getMinecraft(); + private final RenderState renderState; + private DObject entitiesObject; + private Map entityObjects; + + public EntityExporter(RenderState renderState) { + this.renderState = renderState; + } + + @Override + @SneakyThrows + public void setup() throws IOException { + entitiesObject = new DObject(DObject.Type.OB_EMPTY); + entitiesObject.id.name = "Entities"; + entitiesObject.layers = 1 << 1; + BlendState.getState().getScene().base.add(entitiesObject); + + entityObjects = new IdentityHashMap<>(); + } + + public void preEntitiesRender() { + Matrix4f modelView = getGlModelViewMatrix(); + // Entities are rendered relative to the viewer location. + // We however want our Entities object to not move when the viewer does, + // so we position it at 0/0/0 and instead have the entities themselves move more + Matrix4f.translate(new Vector3f( + (float) -mc.getRenderManager().viewerPosX, + (float) -mc.getRenderManager().viewerPosY, + (float) -mc.getRenderManager().viewerPosZ + ), modelView, modelView); + renderState.push(entitiesObject, modelView); + } + + public void postEntitiesRender() { + renderState.pop(); + } + + public void preRender(Entity entity, double dx, double dy, double dz, float yaw, float renderPartialTicks) { + DObject entityObject = entityObjects.get(entity); + if (entityObject == null) { + entityObject = new DObject(DObject.Type.OB_EMPTY); + entityObject.setParent(renderState.peekObject()); + entityObject.id.name = entity.getName(); + entityObjects.put(entity, entityObject); + } + renderState.pushObject(entityObject); + + // Normally the entity renderer will translate to dx/dy/dz but we want that translation outside of + // the entity object (on the entity object itself). + // So, we translate there, then store the model-view-matrix and then translate back to let the + // renderer translate there again. + // Instead of actually translating, we just add the translation on the current model-view-matrix. + Matrix4f modelView = getGlModelViewMatrix(); + Matrix4f.translate(new Vector3f((float) dx, (float) dy, (float) dz), modelView, modelView); + renderState.pushModelView(modelView); + renderState.applyLastModelViewTransformToObject(); + + entityObject.keyframeLocRotScale(renderState.getFrame()); + entityObject.setVisible(renderState.getFrame()); + } + + public void postRender(Entity entity, double dx, double dy, double dz, float yaw, float renderPartialTicks) { + renderState.pop(); + } +} diff --git a/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java new file mode 100644 index 00000000..c420ef03 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java @@ -0,0 +1,111 @@ +package com.replaymod.render.blend.exporters; + +import com.replaymod.render.blend.BlendMeshBuilder; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DMesh; +import com.replaymod.render.blend.data.DObject; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.resources.model.IBakedModel; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumFacing; +import net.minecraftforge.client.model.pipeline.LightUtil; + +import java.util.List; + +public class ItemExporter implements Exporter { + private final RenderState renderState; + + public ItemExporter(RenderState renderState) { + this.renderState = renderState; + } + + public void onRender(IBakedModel model, ItemStack stack) { + DObject object = getObjectForItemStack(model, stack); + + renderState.pushObject(object); + renderState.pushModelView(); + renderState.applyLastModelViewTransformToObject(); + + object.setVisible(renderState.getFrame()); + object.keyframeLocRotScale(renderState.getFrame()); + + renderState.pop(); + } + + private DObject getObjectForItemStack(IBakedModel model, ItemStack stack) { + int frame = renderState.getFrame(); + DObject parent = renderState.peekObject(); + DObject object = null; + for (DObject child : parent.getChildren()) { + if (child.lastFrame < frame + && child instanceof ItemBasedDObject + && ((ItemBasedDObject) child).isBasedOn(model, stack)) { + object = child; + break; + } + } + if (object == null) { + object = new ItemBasedDObject(model, stack); + object.id.name = stack.getDisplayName(); + object.setParent(parent); + } + object.lastFrame = frame; + return object; + } + + @SuppressWarnings("unchecked") + private static DMesh generateMeshForItemStack(IBakedModel model, ItemStack stack) { + DMesh mesh = new DMesh(); + BlendMeshBuilder builder = new BlendMeshBuilder(mesh); + builder.setWellBehaved(true); + builder.startDrawingQuads(); + builder.setVertexFormat(DefaultVertexFormats.ITEM); + + for (EnumFacing face : EnumFacing.values()) { + renderQuads(builder, model.getFaceQuads(face), stack); + } + renderQuads(builder, model.getGeneralQuads(), stack); + + builder.finishDrawing(); + return mesh; + } + + private static void renderQuads(WorldRenderer buffer, List quads, ItemStack stack) { + for (BakedQuad quad : quads) { + int color = stack != null && quad.hasTintIndex() + ? stack.getItem().getColorFromItemStack(stack, quad.getTintIndex()) | 0xff000000 + : 0xffffffff; + LightUtil.renderQuadColor(buffer, quad, color); + } + + } + + private static class ItemBasedDObject extends DObject { + private final IBakedModel model; + private final ItemStack stack; + private boolean valid; + + public ItemBasedDObject(IBakedModel model, ItemStack stack) { + super(generateMeshForItemStack(model, stack)); + this.model = model; + this.stack = stack; + } + + public boolean isBasedOn(IBakedModel model, ItemStack scale) { + return this.model == model; // FIXME ignores color of stack + } + + @Override + public void setVisible(int frame) { + valid = true; + super.setVisible(frame); + } + + @Override + public boolean isValid() { + return valid; + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java new file mode 100644 index 00000000..5f6cfcd2 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java @@ -0,0 +1,106 @@ +package com.replaymod.render.blend.exporters; + +import com.replaymod.render.blend.BlendMeshBuilder; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DMesh; +import com.replaymod.render.blend.data.DObject; +import net.minecraft.client.model.ModelBox; +import net.minecraft.client.model.ModelRenderer; +import org.lwjgl.opengl.GL11; + +import java.io.IOException; + +import static com.replaymod.core.versions.MCVer.*; +import static com.replaymod.render.blend.Util.isGlTextureMatrixIdentity; + +public class ModelRendererExporter implements Exporter { + private final RenderState renderState; + + public ModelRendererExporter(RenderState renderState) { + this.renderState = renderState; + } + + @Override + public void setup() throws IOException { + } + + public void preRenderModel(ModelRenderer model, float scale) { + DObject object = getObjectForModel(model, scale); + renderState.pushObject(object); + renderState.pushModelView(); + } + + public void onRenderModel() { + if (!GL11.glIsEnabled(GL11.GL_TEXTURE_2D) || !isGlTextureMatrixIdentity()) { + return; + } + renderState.popModelView(); + renderState.pushModelView(); + renderState.applyLastModelViewTransformToObject(); + DObject object = renderState.peekObject(); + object.setVisible(renderState.getFrame()); + object.keyframeLocRotScale(renderState.getFrame()); + } + + public void postRenderModel() { + renderState.pop(); + } + + private DObject getObjectForModel(ModelRenderer model, float scale) { + int frame = renderState.getFrame(); + DObject parent = renderState.peekObject(); + DObject object = null; + for (DObject child : parent.getChildren()) { + if (child.lastFrame < frame + && child instanceof ModelBasedDObject + && ((ModelBasedDObject) child).isBasedOn(model, scale)) { + object = child; + break; + } + } + if (object == null) { + object = new ModelBasedDObject(model, scale); + object.id.name = model.boxName; + object.setParent(parent); + } + object.lastFrame = frame; + return object; + } + + private static DMesh generateMesh(ModelRenderer model, float scale) { + DMesh mesh = new DMesh(); + BlendMeshBuilder builder = new BlendMeshBuilder(mesh); + for (ModelBox box : cubeList(model)) { + box.render(builder, scale); + } + builder.maybeFinishDrawing(); + return mesh; + } + + private static class ModelBasedDObject extends DObject { + private final ModelRenderer model; + private final float scale; + private boolean valid; + + public ModelBasedDObject(ModelRenderer model, float scale) { + super(generateMesh(model, scale)); + this.model = model; + this.scale = scale; + } + + public boolean isBasedOn(ModelRenderer model, float scale) { + return this.model == model && Math.abs(this.scale - scale) < 1e-4; + } + + @Override + public void setVisible(int frame) { + valid = true; + super.setVisible(frame); + } + + @Override + public boolean isValid() { + return valid; + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java new file mode 100644 index 00000000..32aa6fe4 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java @@ -0,0 +1,142 @@ +package com.replaymod.render.blend.exporters; + +import com.replaymod.render.blend.BlendMeshBuilder; +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DMesh; +import com.replaymod.render.blend.data.DObject; +import net.minecraft.client.Minecraft; +import net.minecraft.client.particle.EntityFX; +import net.minecraft.entity.Entity; +import org.lwjgl.util.vector.Matrix4f; +import org.lwjgl.util.vector.Vector3f; + +import java.io.IOException; +import java.util.IdentityHashMap; +import java.util.Map; + +import static com.replaymod.render.blend.Util.getGlModelViewMatrix; + +public class ParticlesExporter implements Exporter { + private final Minecraft mc = Minecraft.getMinecraft(); + private final RenderState renderState; + private DObject pointAtObject; + private DObject particlesObject; + private DObject litParticlesObject; + private Map particleObjects; + private Map particleObjectsSeen; + + public ParticlesExporter(RenderState renderState) { + this.renderState = renderState; + } + + @Override + public void setup() throws IOException { + if (true) return; // FIXME make configurable (also other methods below) + // FIXME replace with camera + pointAtObject = new DObject(DObject.Type.OB_EMPTY); + pointAtObject.id.name = "Particles Target"; + pointAtObject.layers = 1 << 2; + BlendState.getState().getScene().base.add(pointAtObject); + + particlesObject = new DObject(DObject.Type.OB_EMPTY); + particlesObject.id.name = "Particles"; + particlesObject.layers = 1 << 1; + BlendState.getState().getScene().base.add(particlesObject); + + litParticlesObject = new DObject(DObject.Type.OB_EMPTY); + litParticlesObject.id.name = "Lit Particles"; + litParticlesObject.layers = 1 << 1; + BlendState.getState().getScene().base.add(litParticlesObject); + + particleObjects = new IdentityHashMap<>(); + particleObjectsSeen = new IdentityHashMap<>(); + } + + public void preParticlesRender(boolean lit) { + if (true) return; + Matrix4f modelView = getGlModelViewMatrix(); + // Particles are rendered relative to the viewer location. + // We however want our Particles object to not move when the viewer does, + // so we position it at 0/0/0 and instead have the particles themselves move more + Matrix4f.translate(new Vector3f( + (float) -mc.getRenderManager().viewerPosX, + (float) -mc.getRenderManager().viewerPosY, + (float) -mc.getRenderManager().viewerPosZ + ), modelView, modelView); + renderState.push(lit ? litParticlesObject : particlesObject, modelView); + } + + public void postParticlesRender() { + if (true) return; + renderState.pop(); + } + + public void onRender(EntityFX particle, float renderPartialTicks) { + if (true) return; + DObject particleObject = particleObjects.get(particle); + if (particleObject == null) { + particleObject = new DObject(DObject.Type.OB_EMPTY); + particleObject.setParent(renderState.peekObject()); + particleObject.pointAt = pointAtObject; + particleObject.layers = 1 << 2; + particleObject.id.name = particle.getClass().getSimpleName(); + particleObject.keyframe("hide", 0, renderState.getFrame() - 1, 1f); + particleObject.keyframe("hide", 0, renderState.getFrame(), 0f); + particleObjects.put(particle, particleObject); + } + particleObjectsSeen.put(particle, particleObject); + renderState.pushObject(particleObject); + + // Normally the particle renderer will translate to dx/dy/dz but we want that translation outside of + // the particle object (on the particle object itself). + // So, we translate there, then store the model-view-matrix and then translate back to let the + // renderer translate there again. + // Instead of actually translating, we just add the translation on the current model-view-matrix. + Matrix4f modelView = getGlModelViewMatrix(); + double dx = particle.prevPosX + (particle.posX - particle.prevPosX) * renderPartialTicks - EntityFX.interpPosX; + double dy = particle.prevPosY + (particle.posY - particle.prevPosY) * renderPartialTicks - EntityFX.interpPosY; + double dz = particle.prevPosZ + (particle.posZ - particle.prevPosZ) * renderPartialTicks - EntityFX.interpPosZ; + Vector3f offset = new Vector3f((float) dx, (float) dy, (float) dz); + Matrix4f.translate(offset, modelView, modelView); + renderState.pushModelView(modelView); + renderState.applyLastModelViewTransformToObject(); + + particleObject.keyframeLoc(renderState.getFrame()); + particleObject.keyframeScale(renderState.getFrame()); + + DObject frameObject = new DObject(generateMeshForParticle(particle, offset)); + frameObject.setParent(particleObject); + frameObject.layers = 1 << 2; + frameObject.id.name = particleObject.id.name + " - Frame " + renderState.getFrame(); + frameObject.keyframe("hide", 0, renderState.getFrame() - 1, 1f); + frameObject.keyframe("hide", 0, renderState.getFrame(), 0f); + frameObject.keyframe("hide", 0, renderState.getFrame() + 1, 1f); + + renderState.pop(); + } + + private DMesh generateMeshForParticle(EntityFX particle, Vector3f offset) { + DMesh mesh = new DMesh(); + BlendMeshBuilder builder = new BlendMeshBuilder(mesh); + builder.setOffset(offset); + builder.setWellBehaved(true); + builder.startDrawingQuads(); + particle.func_180434_a(builder, Minecraft.getMinecraft().getRenderViewEntity(), 0, 1, 1, 0, 0, 0); + builder.finishDrawing(); + return mesh; + } + + @Override + public void postFrame(int frame) throws IOException { + if (true) return; + for (Map.Entry entry : particleObjects.entrySet()) { + if (!particleObjectsSeen.containsKey(entry.getKey())) { + DObject object = entry.getValue(); + object.keyframe("hide", 0, renderState.getFrame(), 1f); + } + } + particleObjects = particleObjectsSeen; + particleObjectsSeen = new IdentityHashMap<>(); + } +} diff --git a/src/main/java/com/replaymod/render/blend/exporters/RenderState.java b/src/main/java/com/replaymod/render/blend/exporters/RenderState.java new file mode 100644 index 00000000..4604cb58 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/RenderState.java @@ -0,0 +1,143 @@ +package com.replaymod.render.blend.exporters; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DMaterial; +import com.replaymod.render.blend.data.DObject; +import org.lwjgl.util.vector.Matrix4f; + +import java.io.IOException; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; + +import static com.replaymod.render.ReplayModRender.LOGGER; +import static com.replaymod.render.blend.Util.getGlModelViewMatrix; +import static com.replaymod.render.blend.Util.glScaleToBlend; +import static com.replaymod.render.blend.Util.glToBlend; +import static com.replaymod.render.blend.Util.posFromMat; +import static com.replaymod.render.blend.Util.rotFromMat; +import static com.replaymod.render.blend.Util.scaleFromMat; +import static com.replaymod.render.blend.Util.scaleMat3; + +public class RenderState implements Exporter { + + private final BlendState blendState; + private final ModelRendererExporter modelExporter = new ModelRendererExporter(this); + private final Deque objectStack = new LinkedList<>(); + private final Deque modelViewStack = new LinkedList<>(); + + private int frame; + + public RenderState(BlendState blendState) { + this.blendState = blendState; + + blendState.register(modelExporter); + } + + public int getFrame() { + return frame; + } + + public DMaterial getCurrentMaterial() { + return blendState.getMaterials().getActiveMaterial(); + } + + public ModelRendererExporter getModelExporter() { + return modelExporter; + } + + public Deque getObjectStack() { + return objectStack; + } + + public void push(DObject object, Matrix4f modelView) { + pushObject(object); + pushModelView(modelView); + } + + public void pushObject(DObject object) { + objectStack.push(object); + } + + public void pushModelView() { + pushModelView(getGlModelViewMatrix()); + } + + public void pushModelView(Matrix4f modelView) { + modelViewStack.push(modelView); + } + + public void pop() { + popObject(); + popModelView(); + } + + public DObject popObject() { + return objectStack.pop(); + } + + public Matrix4f popModelView() { + return modelViewStack.pop(); + } + + public DObject peekObject() { + return objectStack.peek(); + } + + public Matrix4f peekModelView() { + return modelViewStack.peek(); + } + + public void applyLastModelViewTransformToObject() { + applyLastModelViewTransformToObject(peekObject()); + } + + public void applyLastModelViewTransformToObject(DObject object) { + Matrix4f mat = calcLastModelViewTransform(); + + posFromMat(mat, object.loc); + scaleFromMat(mat, object.scale); + scaleMat3(mat, object.scale); + rotFromMat(mat, object.rot); + + glToBlend(object.loc); + glScaleToBlend(object.scale); + glToBlend(object.rot); + } + + public Matrix4f calcLastModelViewTransform() { + Iterator iter = modelViewStack.iterator(); + Matrix4f newModelView = iter.next(); + Matrix4f prevModelView = iter.next(); + // We need to apply the same transformation T to the object that has transformed the old into the new matrix: + // O * T = N <=> T = O^{-1} * N + return Matrix4f.mul(Matrix4f.invert(prevModelView, null), newModelView, null); + } + + @Override + public void setup() throws IOException { + } + + @Override + public void preFrame(int frame) throws IOException { + this.frame = frame; + } + + @Override + public void postFrame(int frame) throws IOException { + if (!objectStack.isEmpty()) { + LOGGER.warn("Post frame with non-empty object stack! ({} remaining)", objectStack.size()); + objectStack.clear(); + } + if (!modelViewStack.isEmpty()) { + LOGGER.warn("Post frame with non-empty model-view stack! ({} remaining)", modelViewStack.size()); + modelViewStack.clear(); + } + } + + @Override + public void tearDown() throws IOException { + + } +} diff --git a/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java b/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java new file mode 100644 index 00000000..aceeefd9 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java @@ -0,0 +1,102 @@ +package com.replaymod.render.blend.exporters; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DObject; +import lombok.SneakyThrows; +import net.minecraft.client.Minecraft; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.BlockPos; +import org.lwjgl.util.vector.Matrix4f; +import org.lwjgl.util.vector.Vector3f; + +import java.io.IOException; +import java.util.IdentityHashMap; +import java.util.Map; + +import static com.replaymod.render.blend.Util.getGlModelViewMatrix; +import static com.replaymod.render.blend.Util.getTileEntityId; + +public class TileEntityExporter implements Exporter { + private final Minecraft mc = Minecraft.getMinecraft(); + private final RenderState renderState; + private DObject tileEntitiesObject; + private Map tileEntityObjects; + private Map tileEntityObjectsSeen; + + public TileEntityExporter(RenderState renderState) { + this.renderState = renderState; + } + + @Override + @SneakyThrows + public void setup() throws IOException { + tileEntitiesObject = new DObject(DObject.Type.OB_EMPTY); + tileEntitiesObject.id.name = "TileEntities"; + tileEntitiesObject.layers = 1 << 1; + BlendState.getState().getScene().base.add(tileEntitiesObject); + + tileEntityObjects = new IdentityHashMap<>(); + tileEntityObjectsSeen = new IdentityHashMap<>(); + } + + public void preTileEntitiesRender() { + Matrix4f modelView = getGlModelViewMatrix(); + // Tile entities are rendered relative to the viewer location. + // We however want our TileEntities object to not move when the viewer does, + // so we position it at 0/0/0 and instead have the tile entities themselves move more + Matrix4f.translate(new Vector3f( + (float) -mc.getRenderManager().viewerPosX, + (float) -mc.getRenderManager().viewerPosY, + (float) -mc.getRenderManager().viewerPosZ + ), modelView, modelView); + renderState.push(tileEntitiesObject, modelView); + } + + public void postTileEntitiesRender() { + renderState.pop(); + } + + public void preRender(TileEntity tileEntity, double dx, double dy, double dz, float renderPartialTicks, int destroyStage) { + DObject tileEntityObject = tileEntityObjects.get(tileEntity); + if (tileEntityObject == null) { + tileEntityObject = new DObject(DObject.Type.OB_EMPTY); + tileEntityObject.setParent(renderState.peekObject()); + BlockPos pos = tileEntity.getPos(); + tileEntityObject.id.name = getTileEntityId(tileEntity) + "( " + pos.getX() + "/" + pos.getY() + "/" + pos.getZ() + ")"; + tileEntityObject.keyframe("hide", 0, renderState.getFrame() - 1, 1f); + tileEntityObject.keyframe("hide", 0, renderState.getFrame(), 0f); + tileEntityObjects.put(tileEntity, tileEntityObject); + } + tileEntityObjectsSeen.put(tileEntity, tileEntityObject); + renderState.pushObject(tileEntityObject); + + // Normally the tile entity renderer will translate to dx/dy/dz but we want that translation outside of + // the tile entity object (on the tile entity object itself). + // So, we translate there, then store the model-view-matrix and then translate back to let the + // renderer translate there again. + // Instead of actually translating, we just add the translation on the current model-view-matrix. + Matrix4f modelView = getGlModelViewMatrix(); + Matrix4f.translate(new Vector3f((float) dx, (float) dy, (float) dz), modelView, modelView); + renderState.pushModelView(modelView); + renderState.applyLastModelViewTransformToObject(); + + tileEntityObject.keyframeLocRotScale(renderState.getFrame()); + } + + public void postRender() { + renderState.pop(); + } + + @Override + public void postFrame(int frame) throws IOException { + for (Map.Entry entry : tileEntityObjects.entrySet()) { + if (!tileEntityObjectsSeen.containsKey(entry.getKey())) { + DObject object = entry.getValue(); + object.keyframe("hide", 0, renderState.getFrame(), 1f); + } + } + tileEntityObjects = tileEntityObjectsSeen; + tileEntityObjectsSeen = new IdentityHashMap<>(); + } +} diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinChunkRenderWorker.java b/src/main/java/com/replaymod/render/blend/mixin/MixinChunkRenderWorker.java new file mode 100644 index 00000000..0c0d5d8a --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinChunkRenderWorker.java @@ -0,0 +1,30 @@ +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ChunkExporter; +import net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator; +import net.minecraft.client.renderer.chunk.ChunkRenderWorker; +import net.minecraft.util.EnumWorldBlockLayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ChunkRenderWorker.class) +public abstract class MixinChunkRenderWorker { + + @Inject(method = "processTask", at = @At("RETURN")) + public void afterChunkUpdate(ChunkCompileTaskGenerator task, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + if (task.getStatus() == ChunkCompileTaskGenerator.Status.DONE + && task.getType() == ChunkCompileTaskGenerator.Type.REBUILD_CHUNK) { + for (EnumWorldBlockLayer layer : EnumWorldBlockLayer.values()) { + if (task.getCompiledChunk().isLayerStarted(layer)) { + blendState.get(ChunkExporter.class).addChunkUpdate(task.getRenderChunk(), layer); + } + } + } + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinEffectRenderer.java b/src/main/java/com/replaymod/render/blend/mixin/MixinEffectRenderer.java new file mode 100644 index 00000000..f9557d97 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinEffectRenderer.java @@ -0,0 +1,46 @@ +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ParticlesExporter; +import net.minecraft.client.particle.EffectRenderer; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EffectRenderer.class) +public abstract class MixinEffectRenderer { + + @Inject(method = "renderParticles", at = @At("HEAD")) + public void preRender(Entity view, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ParticlesExporter.class).preParticlesRender(false); + } + } + + @Inject(method = "renderParticles", at = @At("RETURN")) + public void postRender(Entity view, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ParticlesExporter.class).postParticlesRender(); + } + } + + @Inject(method = "renderLitParticles", at = @At("HEAD")) + public void preLitRender(Entity view, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ParticlesExporter.class).preParticlesRender(true); + } + } + + @Inject(method = "renderLitParticles", at = @At("RETURN")) + public void postLitRender(Entity view, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ParticlesExporter.class).postParticlesRender(); + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinModelRenderer.java b/src/main/java/com/replaymod/render/blend/mixin/MixinModelRenderer.java new file mode 100644 index 00000000..f470ba4f --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinModelRenderer.java @@ -0,0 +1,65 @@ +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ModelRendererExporter; +import net.minecraft.client.model.ModelRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ModelRenderer.class) +public abstract class MixinModelRenderer { + @Inject(method = "render", at = @At("HEAD")) + public void preRender(float scale, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ModelRendererExporter.class).preRenderModel((ModelRenderer)(Object)this, scale); + } + } + + @Inject(method = "renderWithRotation", at = @At("HEAD")) + public void preRenderWithRotation(float scale, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ModelRendererExporter.class).preRenderModel((ModelRenderer)(Object)this, scale); + } + } + + @Inject(method = "render", at = @At("RETURN")) + public void postRender(float scale, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ModelRendererExporter.class).postRenderModel(); + } + } + + @Inject(method = "renderWithRotation", at = @At("RETURN")) + public void postRenderWithRotation(float scale, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ModelRendererExporter.class).postRenderModel(); + } + } + + @Inject(method = "render", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/GlStateManager;callList(I)V"), + expect = 3) + public void onRender(float scale, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ModelRendererExporter.class).onRenderModel(); + } + } + + @Inject(method = "renderWithRotation", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/GlStateManager;callList(I)V")) + public void onRenderWithRotation(float scale, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ModelRendererExporter.class).onRenderModel(); + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java new file mode 100644 index 00000000..4dd38130 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java @@ -0,0 +1,60 @@ +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.EntityExporter; +import com.replaymod.render.blend.exporters.TileEntityExporter; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.culling.ICamera; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(RenderGlobal.class) +public abstract class MixinRenderGlobal { + + // FIXME wither skull ._. mojang pls + + @Inject(method = "renderEntities", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/entity/RenderManager;renderEntitySimple(Lnet/minecraft/entity/Entity;F)V")) + public void preEntityRender(Entity view, ICamera camera, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(EntityExporter.class).preEntitiesRender(); + } + } + + @Inject(method = "renderEntities", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/entity/RenderManager;renderEntitySimple(Lnet/minecraft/entity/Entity;F)V", + shift = At.Shift.AFTER)) + public void postEntityRender(Entity view, ICamera camera, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(EntityExporter.class).postEntitiesRender(); + } + } + + @Inject(method = "renderEntities", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher;renderTileEntity(Lnet/minecraft/tileentity/TileEntity;FI)V")) + public void preTileEntityRender(Entity view, ICamera camera, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(TileEntityExporter.class).preTileEntitiesRender(); + } + } + + @Inject(method = "renderEntities", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher;renderTileEntity(Lnet/minecraft/tileentity/TileEntity;FI)V", + shift = At.Shift.AFTER)) + public void postTileEntityRender(Entity view, ICamera camera, float renderPartialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(TileEntityExporter.class).postTileEntitiesRender(); + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java new file mode 100644 index 00000000..943915ac --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java @@ -0,0 +1,23 @@ +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ItemExporter; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.client.resources.model.IBakedModel; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(RenderItem.class) +public abstract class MixinRenderItem { + @Inject(method = "renderModel(Lnet/minecraft/client/resources/model/IBakedModel;Lnet/minecraft/item/ItemStack;)V", + at = @At("HEAD")) + private void onRenderModel(IBakedModel model, ItemStack stack, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ItemExporter.class).onRender(model, stack); + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java new file mode 100644 index 00000000..450f39e8 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java @@ -0,0 +1,35 @@ +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.EntityExporter; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(RenderManager.class) +public abstract class MixinRenderManager { + + @Inject(method = "doRenderEntity", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/entity/Render;doRender(Lnet/minecraft/entity/Entity;DDDFFZ)V")) + public void preRender(Entity entity, double x, double y, double z, float yaw, float renderPartialTicks, boolean box, CallbackInfoReturnable ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(EntityExporter.class).preRender(entity, x, y, z, yaw, renderPartialTicks); + } + } + + @Inject(method = "doRenderEntity", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/entity/Render;doRender(Lnet/minecraft/entity/Entity;DDDFFZ)V", + shift = At.Shift.AFTER)) + public void postRender(Entity entity, double x, double y, double z, float yaw, float renderPartialTicks, boolean box, CallbackInfoReturnable ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(EntityExporter.class).postRender(entity, x, y, z, yaw, renderPartialTicks); + } + } +} diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinTileEntityRendererDispatcher.java b/src/main/java/com/replaymod/render/blend/mixin/MixinTileEntityRendererDispatcher.java new file mode 100644 index 00000000..74fe86d3 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinTileEntityRendererDispatcher.java @@ -0,0 +1,32 @@ +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.TileEntityExporter; +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.tileentity.TileEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(TileEntityRendererDispatcher.class) +public abstract class MixinTileEntityRendererDispatcher { + + @Inject(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDFI)V", + at = @At("HEAD")) + public void preRender(TileEntity tileEntity, double x, double y, double z, float renderPartialTicks, int destroyStage, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(TileEntityExporter.class).preRender(tileEntity, x, y, z, renderPartialTicks, destroyStage); + } + } + + @Inject(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDFI)V", + at = @At("RETURN")) + public void postRender(TileEntity tileEntity, double x, double y, double z, float renderPartialTicks, int destroyStage, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(TileEntityExporter.class).postRender(); + } + } +} diff --git a/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java b/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java index f6a483cb..6dad7f05 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java +++ b/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java @@ -1,6 +1,8 @@ package com.replaymod.render.mixin; //#if MC<10904 +//$$ import com.replaymod.render.blend.BlendState; +//$$ import com.replaymod.render.blend.exporters.ParticlesExporter; //$$ import com.replaymod.render.hooks.EntityRendererHandler; //$$ import net.minecraft.client.Minecraft; //$$ import net.minecraft.client.particle.EffectRenderer; @@ -50,6 +52,10 @@ package com.replaymod.render.mixin; //$$ //$$ rotYZ = (float) (-rotZ * Math.sin(pitch)); //$$ rotXY = (float) (rotX * Math.sin(pitch)); +//$$ } +//$$ BlendState blendState = BlendState.getState(); +//$$ if (blendState != null) { +//$$ blendState.get(ParticlesExporter.class).onRender(fx, partialTicks); //$$ } //#if MC>=10809 //$$ fx.renderParticle(worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY); diff --git a/src/main/java/com/replaymod/render/processor/DummyProcessor.java b/src/main/java/com/replaymod/render/processor/DummyProcessor.java new file mode 100644 index 00000000..f8320459 --- /dev/null +++ b/src/main/java/com/replaymod/render/processor/DummyProcessor.java @@ -0,0 +1,10 @@ +package com.replaymod.render.processor; + +import com.replaymod.render.rendering.Frame; + +public class DummyProcessor extends AbstractFrameProcessor { + @Override + public F process(F rawFrame) { + return rawFrame; + } +} diff --git a/src/main/java/com/replaymod/render/rendering/Pipelines.java b/src/main/java/com/replaymod/render/rendering/Pipelines.java index f16ba7df..0d4fd80c 100644 --- a/src/main/java/com/replaymod/render/rendering/Pipelines.java +++ b/src/main/java/com/replaymod/render/rendering/Pipelines.java @@ -1,13 +1,32 @@ package com.replaymod.render.rendering; import com.replaymod.render.RenderSettings; -import com.replaymod.render.capturer.*; -import com.replaymod.render.frame.*; +import com.replaymod.render.blend.BlendFrameCapturer; +import com.replaymod.render.capturer.CubicOpenGlFrameCapturer; +import com.replaymod.render.capturer.CubicPboOpenGlFrameCapturer; +import com.replaymod.render.capturer.ODSFrameCapturer; +import com.replaymod.render.capturer.RenderInfo; +import com.replaymod.render.capturer.SimpleOpenGlFrameCapturer; +import com.replaymod.render.capturer.SimplePboOpenGlFrameCapturer; +import com.replaymod.render.capturer.StereoscopicOpenGlFrameCapturer; +import com.replaymod.render.capturer.StereoscopicPboOpenGlFrameCapturer; +import com.replaymod.render.frame.CubicOpenGlFrame; +import com.replaymod.render.frame.ODSOpenGlFrame; +import com.replaymod.render.frame.OpenGlFrame; +import com.replaymod.render.frame.RGBFrame; +import com.replaymod.render.frame.StereoscopicOpenGlFrame; import com.replaymod.render.hooks.EntityRendererHandler; -import com.replaymod.render.processor.*; +import com.replaymod.render.processor.CubicToRGBProcessor; +import com.replaymod.render.processor.DummyProcessor; +import com.replaymod.render.processor.EquirectangularToRGBProcessor; +import com.replaymod.render.processor.ODSToRGBProcessor; +import com.replaymod.render.processor.OpenGlToRGBProcessor; +import com.replaymod.render.processor.StereoscopicToRGBProcessor; import com.replaymod.render.utils.PixelBufferObject; import lombok.experimental.UtilityClass; +import java.io.IOException; + @UtilityClass public class Pipelines { public static Pipeline newPipeline(RenderSettings.RenderMethod method, RenderInfo renderInfo, FrameConsumer consumer) { @@ -22,6 +41,8 @@ public class Pipelines { return newEquirectangularPipeline(renderInfo, consumer); case ODS: return newODSPipeline(renderInfo, consumer); + case BLEND: + throw new UnsupportedOperationException("Use newBlendPipeline instead!"); } throw new UnsupportedOperationException("Unknown method: " + method); } @@ -76,4 +97,19 @@ public class Pipelines { new ODSFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4); return new Pipeline<>(capturer, new ODSToRGBProcessor(settings.getVideoWidth() / 4), consumer); } + + public static Pipeline newBlendPipeline(RenderInfo renderInfo) throws IOException { + RenderSettings settings = renderInfo.getRenderSettings(); + FrameCapturer capturer = new BlendFrameCapturer(new EntityRendererHandler(settings), renderInfo); + FrameConsumer consumer = new FrameConsumer() { + @Override + public void consume(RGBFrame frame) { + } + + @Override + public void close() throws IOException { + } + }; + return new Pipeline<>(capturer, new DummyProcessor<>(), consumer); + } } diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 2854fd9e..cb3286ec 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -7,6 +7,7 @@ import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; import com.replaymod.render.VideoWriter; +import com.replaymod.render.blend.BlendState; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.events.ReplayRenderEvent; import com.replaymod.render.frame.RGBFrame; @@ -46,7 +47,10 @@ import java.util.concurrent.FutureTask; import static com.google.common.collect.Iterables.getLast; import static com.replaymod.core.versions.MCVer.*; import static com.replaymod.render.ReplayModRender.LOGGER; -import static net.minecraft.client.renderer.GlStateManager.*; +import static net.minecraft.client.renderer.GlStateManager.clear; +import static net.minecraft.client.renderer.GlStateManager.enableTexture2D; +import static net.minecraft.client.renderer.GlStateManager.popMatrix; +import static net.minecraft.client.renderer.GlStateManager.pushMatrix; import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; @@ -87,14 +91,21 @@ public class VideoRenderer implements RenderInfo { this.replayHandler = replayHandler; this.timeline = timeline; this.gui = new GuiVideoRenderer(this); - this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this, - videoWriter = new VideoWriter(this) { - @Override - public void consume(RGBFrame frame) { - gui.updatePreview(frame); - super.consume(frame); - } - }); + if (settings.getRenderMethod() == RenderSettings.RenderMethod.BLEND) { + BlendState.setState(new BlendState(settings.getOutputFile())); + + this.renderingPipeline = Pipelines.newBlendPipeline(this); + this.videoWriter = null; + } else { + this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this, + videoWriter = new VideoWriter(this) { + @Override + public void consume(RGBFrame frame) { + gui.updatePreview(frame); + super.consume(frame); + } + }); + } } /** @@ -284,7 +295,7 @@ public class VideoRenderer implements RenderInfo { new SoundHandler().playRenderSuccessSound(); try { - if (!hasFailed()) { + if (!hasFailed() && videoWriter != null) { new GuiRenderingDone(ReplayModRender.instance, videoWriter.getVideoFile(), totalFrames, settings).display(); } } catch (VideoWriter.FFmpegStartupException e) { @@ -401,7 +412,9 @@ public class VideoRenderer implements RenderInfo { } public void cancel() { - videoWriter.abort(); + if (videoWriter != null) { + videoWriter.abort(); + } this.cancelled = true; renderingPipeline.cancel(); } diff --git a/src/main/resources/mixins.render.blend.replaymod.json b/src/main/resources/mixins.render.blend.replaymod.json new file mode 100644 index 00000000..3dcc3306 --- /dev/null +++ b/src/main/resources/mixins.render.blend.replaymod.json @@ -0,0 +1,18 @@ +{ + "required": true, + "package": "com.replaymod.render.blend.mixin", + "mixins": [], + "server": [], + "client": [ + "MixinChunkRenderWorker", + "MixinEffectRenderer", + "MixinModelRenderer", + "MixinRenderGlobal", + "MixinRenderItem", + "MixinRenderManager", + "MixinTileEntityRendererDispatcher" + ], + "compatibilityLevel": "JAVA_8", + "minVersion": "0.6.11", + "refmap": "mixins.replaymod.refmap.json" +} \ No newline at end of file diff --git a/versions/common.gradle b/versions/common.gradle index 81d78a7f..3c1b79f6 100644 --- a/versions/common.gradle +++ b/versions/common.gradle @@ -77,6 +77,8 @@ dependencies { shade 'org.aspectj:aspectjrt:1.8.2' + shade 'com.github.ReplayMod.JavaBlend:2.79.0:e0a1b9a' + def studioVersion = project.name if (studioVersion == '1.8.9') studioVersion = '1.8' shade("com.github.ReplayMod.ReplayStudio:$studioVersion:9fd96a7"){