diff --git a/jGui b/jGui index 79b0aaf3..8019dfd1 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 79b0aaf3e9b5c70739c30a9ce1af9efa8adb26dc +Subproject commit 8019dfd11a0afb87e4ed19fc4056b0fb3973788f diff --git a/src/main/java/com/replaymod/core/LoadingPlugin.java b/src/main/java/com/replaymod/core/LoadingPlugin.java index a1c78347..89b1f086 100755 --- a/src/main/java/com/replaymod/core/LoadingPlugin.java +++ b/src/main/java/com/replaymod/core/LoadingPlugin.java @@ -31,6 +31,7 @@ package com.replaymod.core; //$$ 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 0a199fa7..95bc43b6 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -12,6 +12,7 @@ import net.minecraft.client.multiplayer.WorldClient; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.renderer.texture.TextureMap; import net.minecraft.client.settings.KeyBinding; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; @@ -31,11 +32,15 @@ import net.minecraftforge.common.MinecraftForge; //#if MC>=11300 import net.minecraft.client.MainWindow; +import net.minecraft.client.renderer.entity.model.ModelBox; +import net.minecraft.client.renderer.entity.model.ModelRenderer; import net.minecraft.client.util.InputMappings; import net.minecraft.crash.ReportedException; import org.lwjgl.glfw.GLFW; //#else //$$ import net.minecraft.client.gui.ScaledResolution; +//$$ import net.minecraft.client.model.ModelBox; +//$$ import net.minecraft.client.model.ModelRenderer; //$$ import net.minecraft.client.resources.ResourcePackRepository; //$$ import net.minecraft.util.ReportedException; //$$ import org.apache.logging.log4j.LogManager; @@ -50,8 +55,10 @@ import net.minecraft.client.renderer.BufferBuilder; //#else //$$ import net.minecraft.client.renderer.VertexBuffer; //#endif +import net.minecraft.client.particle.Particle; import net.minecraft.util.math.MathHelper; //#else +//$$ import net.minecraft.client.particle.EntityFX; //#if MC>=10800 //$$ import net.minecraft.client.renderer.WorldRenderer; //#endif @@ -70,6 +77,8 @@ import net.minecraft.client.renderer.vertex.DefaultVertexFormats; //#if MC>=10800 import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.GlStateManager.BooleanState; +import net.minecraft.client.renderer.vertex.VertexFormat; +import net.minecraft.client.renderer.vertex.VertexFormatElement; import net.minecraft.world.WorldType; //#if MC>=11300 import net.minecraftforge.eventbus.api.IEventBus; @@ -287,6 +296,13 @@ public class MCVer { //$$ } //#endif + public static ResourceLocation LOCATION_BLOCKS_TEXTURE + //#if MC>=10904 + = TextureMap.LOCATION_BLOCKS_TEXTURE; + //#else + //$$ = TextureMap.locationBlocksTexture; + //#endif + public static EntityPlayerSP player(Minecraft mc) { //#if MC>=11102 return mc.player; @@ -341,6 +357,11 @@ public class MCVer { //#endif } + @SuppressWarnings("unchecked") + public static List cubeList(ModelRenderer modelRenderer) { + return modelRenderer.cubeList; + } + @SuppressWarnings("unchecked") public static List playerEntities(World world) { return world.playerEntities; @@ -590,6 +611,29 @@ public class MCVer { //#endif } + //#if MC>=10800 + //#if MC>=10904 + //#if MC>=11200 + public static BufferBuilder getBuffer(Tessellator tessellator) { + //#else + //$$ public static VertexBuffer getBuffer(Tessellator tessellator) { + //#endif + //#else + //$$ public static WorldRenderer getBuffer(Tessellator tessellator) { + //#endif + //#if MC>=10904 + return Tessellator.getInstance().getBuffer(); + //#else + //$$ return Tessellator.getInstance().getWorldRenderer(); + //#endif + } + + @SuppressWarnings("unchecked") + public static List getElements(VertexFormat vertexFormat) { + return vertexFormat.getElements(); + } + //#endif + public static Tessellator Tessellator_getInstance() { //#if MC>=10800 return Tessellator.getInstance(); @@ -654,6 +698,30 @@ public class MCVer { return MathHelper.sin(val); } + public static double interpPosX() { + //#if MC>=10904 + return Particle.interpPosX; + //#else + //$$ return EntityFX.interpPosX; + //#endif + } + + public static double interpPosY() { + //#if MC>=10904 + return Particle.interpPosY; + //#else + //$$ return EntityFX.interpPosY; + //#endif + } + + public static double interpPosZ() { + //#if MC>=10904 + return Particle.interpPosZ; + //#else + //$$ return EntityFX.interpPosZ; + //#endif + } + public static ReportedException newReportedException(CrashReport crashReport) { return new ReportedException(crashReport); } diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java index 54f1dada..c7013125 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java @@ -2,6 +2,7 @@ package com.replaymod.extras.advancedscreenshots; import com.replaymod.core.versions.MCVer; import com.replaymod.render.RenderSettings; +import com.replaymod.render.blend.BlendState; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.rendering.Pipelines; import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; @@ -40,8 +41,13 @@ public class ScreenshotRenderer implements RenderInfo { ChunkLoadingRenderGlobal clrg = new ChunkLoadingRenderGlobal(mc.renderGlobal); //#endif - 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(); + } //#if MC>=10800 clrg.uninstall(); diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index a675fecd..5ff3f058 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -28,7 +28,7 @@ import org.apache.maven.artifact.versioning.ComparableVersion; @Data public class RenderSettings { public enum RenderMethod { - DEFAULT, STEREOSCOPIC, CUBIC, EQUIRECTANGULAR, ODS; + DEFAULT, STEREOSCOPIC, CUBIC, EQUIRECTANGULAR, ODS, BLEND; @Override public String toString() { @@ -61,6 +61,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; @@ -80,10 +82,10 @@ public class RenderSettings { } public boolean hasBitrateSetting() { - return preset.contains("%BITRATE%"); + return preset != null && preset.contains("%BITRATE%"); } - public boolean isYuv420() { return preset.contains("-pix_fmt yuv420p"); } + public boolean isYuv420() { return preset != null && preset.contains("-pix_fmt yuv420p"); } @Override public String toString() { 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..3627c9d0 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/BlendFrameCapturer.java @@ -0,0 +1,48 @@ +package com.replaymod.render.blend; + +import com.replaymod.core.versions.MCVer; +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 de.johni0702.minecraft.gui.utils.lwjgl.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(MCVer.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..62c6c876 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/BlendMeshBuilder.java @@ -0,0 +1,276 @@ +//#if MC>=10800 +package com.replaymod.render.blend; + +import com.replaymod.render.blend.data.DMaterial; +import com.replaymod.render.blend.data.DMesh; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.ReadableVector3f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector2f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.VertexFormat; +import net.minecraft.client.renderer.vertex.VertexFormatElement; +import org.lwjgl.opengl.GL11; + +//#if MC>=10904 +//#if MC>=11200 +import net.minecraft.client.renderer.BufferBuilder; +//#else +//$$ import net.minecraft.client.renderer.VertexBuffer; +//#endif +//#else +//$$ import net.minecraft.client.renderer.WorldRenderer; +//#endif + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static com.replaymod.core.versions.MCVer.*; + +public class BlendMeshBuilder + //#if MC>=10904 + //#if MC>=11200 + extends BufferBuilder + //#else + //$$ extends VertexBuffer + //#endif + //#else + //$$ extends WorldRenderer + //#endif +{ + private static final ReadableVector3f VEC3F_ZERO = new Vector3f(0, 0, 0); + private final DMesh mesh; + private Vector3f offset; + private boolean isDrawing; + private boolean wellBehaved; + + public BlendMeshBuilder(DMesh mesh) { + super(1024); + this.mesh = mesh; + } + + public void setWellBehaved(boolean wellBehaved) { + this.wellBehaved = wellBehaved; + } + + public void setOffset(Vector3f offset) { + this.offset = offset; + } + + @Override + //#if MC>=10809 + public void begin(int mode, VertexFormat vertexFormat) { + //#else + //$$ public void startDrawing(int mode) { + //#endif + 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) + super.finishDrawing(); + addBufferToMesh(); + } else { + throw new IllegalStateException("Already drawing!"); + } + } + this.isDrawing = true; + + if (!wellBehaved) { + // In case the calling code finishes with Tessellator.getInstance().draw() + BufferBuilder_beginPosTexCol(mode); + } + + //#if MC>=10809 + super.begin(mode, vertexFormat); + //#else + //$$ super.startDrawing(mode); + //#endif + } + + public void maybeFinishDrawing() { + if (isDrawing) { + isDrawing = false; + super.finishDrawing(); + addBufferToMesh(); + } + } + + @Override + //#if MC>=10809 + public void finishDrawing() { + //#else + //$$ public int finishDrawing() { + //#endif + if (!isDrawing) { + throw new IllegalStateException("Not building!"); + } else { + if (!wellBehaved) { + getBuffer(Tessellator.getInstance()).finishDrawing(); + } + + //#if MC<10809 + //$$ int ret = + //#endif + super.finishDrawing(); + + addBufferToMesh(); + + //#if MC<10809 + //$$ return ret; + //#endif + } + } + + private void addBufferToMesh() { + addBufferToMesh(this, mesh, offset); + } + + //#if MC>=10904 + //#if MC>=11200 + public static DMesh addBufferToMesh(BufferBuilder bufferBuilder, DMesh mesh, ReadableVector3f vertOffset) { + //#else + //$$ public static DMesh addBufferToMesh(VertexBuffer bufferBuilder, DMesh mesh, Vector3f vertOffset) { + //#endif + //#else + //$$ public static DMesh addBufferToMesh(WorldRenderer bufferBuilder, DMesh mesh, Vector3f vertOffset) { + //#endif + return addBufferToMesh(bufferBuilder.getByteBuffer(), bufferBuilder.getDrawMode(), bufferBuilder.getVertexFormat(), mesh, vertOffset); + } + + public static DMesh addBufferToMesh(ByteBuffer buffer, int mode, VertexFormat vertexFormat, DMesh mesh, ReadableVector3f vertOffset) { + //#if MC>=11300 + int vertexCount = buffer.remaining() / vertexFormat.getSize(); + //#else + //$$ int vertexCount = buffer.remaining() / vertexFormat.getNextOffset(); + //#endif + return addBufferToMesh(buffer, vertexCount, mode, vertexFormat, mesh, vertOffset); + } + + public static DMesh addBufferToMesh(ByteBuffer buffer, int vertexCount, int mode, VertexFormat vertexFormat, DMesh mesh, ReadableVector3f vertOffset) { + if (mesh == null) { + mesh = new DMesh(); + } + if (vertOffset == null) { + vertOffset = VEC3F_ZERO; + } + + // Determine offset of vertex components + int posOffset = -1, colorOffset = -1, uvOffset = -1; + int index = 0; + for (VertexFormatElement element : getElements(vertexFormat)) { + //#if MC>=10809 + int offset = vertexFormat.getOffset(index); + //#else + //$$ int offset = element.getOffset(); + //#endif + switch (element.getUsage()) { + case POSITION: + if (element.getType() != VertexFormatElement.EnumType.FLOAT) { + throw new UnsupportedOperationException("Only float is supported for position elements!"); + } + posOffset = offset; + break; + case COLOR: + if (element.getType() != VertexFormatElement.EnumType.UBYTE) { + throw new UnsupportedOperationException("Only unsigned byte is supported for color elements!"); + } + colorOffset = offset; + break; + case UV: + if (element.getIndex() != 0) break; + if (element.getType() != VertexFormatElement.EnumType.FLOAT) { + throw new UnsupportedOperationException("Only float is supported for UV elements!"); + } + uvOffset = offset; + break; + } + index++; + } + if (posOffset == -1) throw new IllegalStateException("No position element in " + vertexFormat); + + // Extract vertex components from byte buffer + List vertices = new ArrayList<>(vertexCount); + List uvs = new ArrayList<>(vertexCount); + List colors = new ArrayList<>(vertexCount); + //#if MC>=11300 + int step = vertexFormat.getSize(); + //#else + //$$ int step = vertexFormat.getNextOffset(); + //#endif + for (int offset = 0; offset < vertexCount * step; offset += step) { + vertices.add(new DMesh.Vertex( + buffer.getFloat(offset ) - vertOffset.getX(), + -buffer.getFloat(offset + 8) + vertOffset.getZ(), + buffer.getFloat(offset + 4) - vertOffset.getY() + )); + + if (colorOffset != -1) { + colors.add(buffer.getInt(offset + colorOffset)); + } else { + colors.add(0xffffffff); + } + + if (uvOffset != -1) { + uvs.add(new Vector2f( + buffer.getFloat(offset + uvOffset), + 1 - buffer.getFloat(offset + uvOffset + 4) + )); + } else { + uvs.add(new Vector2f(0, 0)); + } + } + + // Determine and store current material + DMaterial activeMaterial = BlendState.getState().getMaterials().getActiveMaterial(); + int materialSlot = mesh.materials.indexOf(activeMaterial); + if (materialSlot < 0) { + materialSlot = mesh.materials.size(); + mesh.materials.add(activeMaterial); + } + + // Bundle vertices into shapes and add them to the mesh + 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); + } + + return mesh; + } +} +//#endif 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..1eab7fd4 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/BlendState.java @@ -0,0 +1,174 @@ +package com.replaymod.render.blend; + +import com.replaymod.render.blend.data.DScene; +import com.replaymod.render.blend.data.Serializer; +//#if MC>=10800 +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; +//#endif +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +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.core.versions.MCVer.*; +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); + + //#if MC>=10800 + 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)); + //#endif + } + + 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"); + addDetail(category, "Exporter", exporter::toString); + throw newReportedException(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"); + addDetail(category, "Exporter", exporter::toString); + throw newReportedException(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"); + addDetail(category, "Exporter", exporter::toString); + addDetail(category, "Frame", () -> String.valueOf(frame)); + throw newReportedException(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"); + addDetail(category, "Exporter", exporter::toString); + addDetail(category, "Frame", () -> String.valueOf(frame)); + throw newReportedException(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..4f10c55c --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/Util.java @@ -0,0 +1,189 @@ +package com.replaymod.render.blend; + +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Matrix3f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Matrix4f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Quaternion; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +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 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(); + //#if MC>=11300 + GL11.glGetFloatv(matrix, floatBuffer); + //#else + //$$ GL11.glGetFloat(matrix, floatBuffer); + //#endif + 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(); + //#if MC>=11300 + tileEntity.write(nbt); + //#else + //$$ tileEntity.writeToNBT(nbt); + //#endif + 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..677f8ad8 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/DMesh.java @@ -0,0 +1,320 @@ +package com.replaymod.render.blend.data; + +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector2f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +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 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 float getSizeX() { + float minX = Float.POSITIVE_INFINITY, maxX = Float.NEGATIVE_INFINITY; + for (Vertex vertex : vertices) { + if (vertex.pos.x < minX) { + minX = vertex.pos.x; + } + if (vertex.pos.x > maxX) { + maxX = vertex.pos.x; + } + } + return maxX - minX; + } + + public boolean hasZeroLengthEdge(float delta) { + float deltaSquared = delta * delta; + Vector3f dst = new Vector3f(); + for (Edge edge : edges) { + Vertex v1 = vertices.get(edge.v1); + Vertex v2 = vertices.get(edge.v2); + if (Vector3f.sub(v1.pos, v2.pos, dst).lengthSquared() < deltaSquared) { + return true; + } + } + return false; + } + + public static class Vertex { + public Vector3f pos; + public short normX, normY, normZ; + + 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.getX()); + pos.set(1, this.pos.getY()); + pos.set(2, this.pos.getZ()); + CArrayFacade norm = mVert.getNo(); + norm.set(0, this.normX); + norm.set(1, this.normY); + norm.set(2, this.normZ); + } + } + + 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..c375dfc3 --- /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 de.johni0702.minecraft.gui.utils.lwjgl.vector.Quaternion; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +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 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..fb54292e --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/data/Serializer.java @@ -0,0 +1,132 @@ +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 Map usedNames = new HashMap<>(); + + 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; + int counter = usedNames.compute(name, (n, i) -> i == null ? 0 : i + 1); + String fullName = counter == 0 ? name : name + "." + counter; + 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..ef0d4a65 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ChunkExporter.java @@ -0,0 +1,182 @@ +//#if MC>=10800 +package com.replaymod.render.blend.exporters; + +import com.replaymod.core.versions.MCVer; +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 com.replaymod.replaystudio.us.myles.ViaVersion.util.ReflectionUtil; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +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.chunk.CompiledChunk; +import net.minecraft.client.renderer.chunk.RenderChunk; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.renderer.vertex.VertexBuffer; +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; + +//#if MC>=11300 +import net.minecraft.client.renderer.WorldRenderer.ContainerLocalRenderInformation; +//#else +//$$ import net.minecraft.client.renderer.RenderGlobal.ContainerLocalRenderInformation; +//#endif + +//#if MC>=10904 +import net.minecraft.util.BlockRenderLayer; +import net.minecraft.util.math.BlockPos; +//#else +//$$ import net.minecraft.util.BlockPos; +//$$ import net.minecraft.util.EnumWorldBlockLayer; +//#endif + +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; + +import static com.replaymod.core.versions.MCVer.*; + +public class ChunkExporter implements Exporter { + private final Map chunkObjects = new HashMap<>(); + //#if MC>=10904 + private final Map> chunkLayerObjects = new HashMap<>(); + private final List> chunks = new ArrayList<>(); + //#else + //$$ private final Map> chunkLayerObjects = new HashMap<>(); + //$$ private final List> chunks = new ArrayList<>(); + //#endif + private DObject chunksObject; + private int frame; + + public void addChunkUpdate(RenderChunk chunk, CompiledChunk compiledChunk) { + //#if MC>=10904 + for (BlockRenderLayer layer : BlockRenderLayer.values()) { + //#else + //$$ for (EnumWorldBlockLayer layer : EnumWorldBlockLayer.values()) { + //#endif + if (compiledChunk == null || compiledChunk.isLayerStarted(layer)) { + chunks.add(Pair.of(chunk, layer)); + } + } + } + + @Override + @SneakyThrows + public void setup() throws IOException { + + Minecraft mc = MCVer.getMinecraft(); + @SuppressWarnings("unchecked") + List renderInfos = mc.renderGlobal.renderInfos; + for (ContainerLocalRenderInformation renderInfo : renderInfos) { + RenderChunk renderChunk = ReflectionUtil.get(renderInfo, "renderChunk", RenderChunk.class); // SneakyThrows + CompiledChunk compiledChunk = renderChunk.getCompiledChunk(); + if (!compiledChunk.isEmpty()) { + addChunkUpdate(renderChunk, null); + } + } + + 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 { + //#if MC>=10904 + for (Pair pair : chunks) { + BlockRenderLayer layer = pair.getRight(); + //#else + //$$ for (Pair pair : chunks) { + //$$ EnumWorldBlockLayer layer = pair.getRight(); + //#endif + RenderChunk chunk = pair.getLeft(); + DObject chunkObject = chunkObjects.get(chunk.getPosition()); + if (chunkObject == null) { + chunkObject = buildChunkObject(chunk); + chunkObjects.put(chunk.getPosition(), chunkObject); + chunkLayerObjects.put(chunk.getPosition(), new EnumMap<>(layer.getDeclaringClass())); + } + DObject layerObject = buildChunkLayerObject(chunkObject, chunk, layer); + if (layerObject == null) continue; + //#if MC>=10904 + Map layerObjects + //#else + //$$ Map layerObjects + //#endif + = 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, + //#if MC>=10904 + BlockRenderLayer layer) { + //#else + //$$ EnumWorldBlockLayer layer) { + //#endif + 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(); + + MCVer.getMinecraft().getTextureManager().bindTexture(LOCATION_BLOCKS_TEXTURE); + + return BlendMeshBuilder.addBufferToMesh(byteBuffer, GL11.GL_QUADS, DefaultVertexFormats.BLOCK, null, null); + } + + private static final int STRIDE = 28; +} +//#endif 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..c671e3f9 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/EntityExporter.java @@ -0,0 +1,102 @@ +//#if MC>=10800 +package com.replaymod.render.blend.exporters; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DObject; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Matrix4f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +import lombok.SneakyThrows; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; + +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 = MCVer.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()); + //#if MC>=11300 + entityObject.id.name = entity.getName().getString(); + //#else + //$$ entityObject.id.name = entity.getName(); + //#endif + 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. + if (!(entity instanceof EntityLivingBase)) { // except for EntityLivingBase which get special (better) treatment + 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 postEntityLivingSetup() { + // For any entity which extends EntityLivingBase, we capture its rotation and scale after it has been setup + // in addition to its position (which we capture in preRender for other entities). + // We could add custom hooks for various other entities types as well but it's probably not worth it. + renderState.pushModelView(); + renderState.applyLastModelViewTransformToObject(); + renderState.peekObject().keyframeLocRotScale(renderState.getFrame()); + } + + public void postRender(Entity entity, double dx, double dy, double dz, float yaw, float renderPartialTicks) { + renderState.pop(); + } +} +//#endif 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..1f6eb509 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ItemExporter.java @@ -0,0 +1,159 @@ +//#if MC>=10800 +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.ItemRenderer; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumFacing; +import net.minecraftforge.client.model.pipeline.LightUtil; +import org.lwjgl.opengl.GL11; + +//#if MC>=11300 +import net.minecraft.client.renderer.model.BakedQuad; +import net.minecraft.client.renderer.model.IBakedModel; +//#else +//$$ import net.minecraft.client.renderer.block.model.BakedQuad; +//#if MC>=10904 +//$$ import net.minecraft.client.renderer.RenderItem; +//$$ import net.minecraft.client.renderer.block.model.IBakedModel; +//#else +//$$ import net.minecraft.client.renderer.entity.RenderItem; +//$$ import net.minecraft.client.resources.model.IBakedModel; +//#endif +//#endif + +import java.util.List; +import java.util.Random; + +public class ItemExporter implements Exporter { + private final RenderState renderState; + + public ItemExporter(RenderState renderState) { + this.renderState = renderState; + } + + public void onRender(Object renderItem, IBakedModel model, ItemStack stack) { + DObject object = getObjectForItemStack(renderItem, model, stack); + + renderState.pushObject(object); + renderState.pushModelView(); + renderState.applyLastModelViewTransformToObject(); + + object.setVisible(renderState.getFrame()); + object.keyframeLocRotScale(renderState.getFrame()); + + renderState.pop(); + } + + private DObject getObjectForItemStack(Object renderItem, 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(renderItem, model, stack)) { + object = child; + break; + } + } + if (object == null) { + object = new ItemBasedDObject(renderItem, model, stack); + //#if MC>=11300 + object.id.name = stack.getDisplayName().getString(); + //#else + //$$ object.id.name = stack.getDisplayName(); + //#endif + object.setParent(parent); + } + object.lastFrame = frame; + return object; + } + + @SuppressWarnings("unchecked") + private static DMesh generateMeshForItemStack(Object renderItem, IBakedModel model, ItemStack stack) { + DMesh mesh = new DMesh(); + BlendMeshBuilder builder = new BlendMeshBuilder(mesh); + builder.setWellBehaved(true); + //#if MC>=10809 + builder.begin(GL11.GL_QUADS, DefaultVertexFormats.ITEM); + //#else + //$$ builder.startDrawingQuads(); + //$$ builder.setVertexFormat(DefaultVertexFormats.ITEM); + //#endif + + //#if MC>=11300 + for (EnumFacing face : EnumFacing.values()) { + renderQuads(renderItem, builder, model.getQuads(null, face, new Random()), stack); + } + renderQuads(renderItem, builder, model.getQuads(null, null, new Random()), stack); + //#else + //#if MC>=10904 + //$$ for (EnumFacing face : EnumFacing.values()) { + //$$ renderQuads(renderItem, builder, model.getQuads(null, face, 0), stack); + //$$ } + //$$ renderQuads(renderItem, builder, model.getQuads(null, null, 0), stack); + //#else + //$$ for (EnumFacing face : EnumFacing.values()) { + //$$ renderQuads(renderItem, builder, model.getFaceQuads(face), stack); + //$$ } + //$$ renderQuads(renderItem, builder, model.getGeneralQuads(), stack); + //#endif + //#endif + + builder.finishDrawing(); + return mesh; + } + + private static void renderQuads(Object renderItem, BlendMeshBuilder buffer, List quads, ItemStack stack) { + for (BakedQuad quad : quads) { + int color = stack != null && quad.hasTintIndex() + //#if MC>=11300 + ? ((ItemRenderer) renderItem).itemColors.getColor(stack, quad.getTintIndex()) | 0xff000000 + //#else + //#if MC>=10904 + //$$ ? ((RenderItem) renderItem).itemColors.getColorFromItemstack(stack, quad.getTintIndex()) | 0xff000000 + //#else + //$$ ? stack.getItem().getColorFromItemStack(stack, quad.getTintIndex()) | 0xff000000 + //#endif + //#endif + : 0xffffffff; + LightUtil.renderQuadColor(buffer, quad, color); + } + + } + + private static class ItemBasedDObject extends DObject { + private final Object renderItem; + private final IBakedModel model; + private final ItemStack stack; + private boolean valid; + + public ItemBasedDObject(Object renderItem, IBakedModel model, ItemStack stack) { + super(generateMeshForItemStack(renderItem, model, stack)); + this.renderItem = renderItem; + this.model = model; + this.stack = stack; + } + + public boolean isBasedOn(Object renderItem, IBakedModel model, ItemStack stack) { + return this.renderItem == renderItem && this.model == model && this.stack == stack; + } + + @Override + public void setVisible(int frame) { + valid = true; + super.setVisible(frame); + } + + @Override + public boolean isValid() { + return valid; + } + } +} +//#endif 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..a9d5efd8 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java @@ -0,0 +1,114 @@ +//#if MC>=10800 +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 org.lwjgl.opengl.GL11; + +//#if MC>=11300 +import net.minecraft.client.renderer.entity.model.ModelBox; +import net.minecraft.client.renderer.entity.model.ModelRenderer; +//#else +//$$ import net.minecraft.client.model.ModelBox; +//$$ import net.minecraft.client.model.ModelRenderer; +//#endif + +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; + } + } +} +//#endif 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..25b90fb1 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/ParticlesExporter.java @@ -0,0 +1,185 @@ +//#if MC>=10800 +package com.replaymod.render.blend.exporters; + +import com.replaymod.core.versions.MCVer; +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 de.johni0702.minecraft.gui.utils.lwjgl.vector.Matrix4f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +import net.minecraft.client.Minecraft; + +//#if MC>=10904 +import net.minecraft.client.particle.Particle; +//#else +//$$ import net.minecraft.client.particle.EntityFX; +//$$ import net.minecraft.entity.Entity; +//#endif + +//#if MC>=10809 +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +//#endif + +import java.io.IOException; +import java.util.IdentityHashMap; +import java.util.Map; + +import static com.replaymod.core.versions.MCVer.*; +import static com.replaymod.render.blend.Util.getGlModelViewMatrix; + +public class ParticlesExporter implements Exporter { + private final Minecraft mc = MCVer.getMinecraft(); + private final RenderState renderState; + private DObject pointAtObject; + private DObject particlesObject; + private DObject litParticlesObject; + //#if MC>=10904 + private Map particleObjects; + private Map particleObjectsSeen; + //#else + //$$ private Map particleObjects; + //$$ private Map particleObjectsSeen; + //#endif + + public ParticlesExporter(RenderState renderState) { + this.renderState = renderState; + } + + @Override + public void setup() throws IOException { + // 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) { + 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() { + renderState.pop(); + } + + //#if MC>=10904 + public void onRender(Particle particle, float renderPartialTicks) { + //#else + //$$ public void onRender(EntityFX particle, float renderPartialTicks) { + //#endif + DObject particleObject = particleObjects.get(particle); + if (particleObject == null) { + particleObject = new DObject(DObject.Type.OB_EMPTY); // mesh generation is delayed, see below + 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 - interpPosX(); + double dy = particle.prevPosY + (particle.posY - particle.prevPosY) * renderPartialTicks - interpPosY(); + double dz = particle.prevPosZ + (particle.posZ - particle.prevPosZ) * renderPartialTicks - 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()); + + DMesh mesh = generateMeshForParticle(particle, offset); + if (mesh.vertices.isEmpty()) { + renderState.pop(); + return; // no particle to be found? + } + if (particleObject.mesh == null) { + // Some particles start out as single points, so we can never scale those first meshes to other sizes + if (mesh.hasZeroLengthEdge(0.1f)) { + // therefore we must further delay mesh generation in these cases and assume a current model scale of 0 + particleObject.scale.set(0, 0, 0); + } else { + // until we find one proper mesh which we can then use as the "unit" mesh + particleObject.type = DObject.Type.OB_MESH; + particleObject.mesh = mesh; + particleObject.scale.set(1, 1, 1); + } + } else { + // Determine relative scale of current mesh to stored "unit" mesh + // Note: currently assumes the same scale on all relevant axes (should be true for most particles?). + float unitSize = particleObject.mesh.getSizeX(); + float currentSize = mesh.getSizeX(); + float relativeScale = currentSize / unitSize; + particleObject.scale.set(relativeScale, relativeScale, relativeScale); + } + particleObject.keyframeScale(renderState.getFrame()); + + renderState.pop(); + } + + //#if MC>=10904 + private DMesh generateMeshForParticle(Particle particle, Vector3f offset) { + //#else + //$$ private DMesh generateMeshForParticle(EntityFX particle, Vector3f offset) { + //#endif + DMesh mesh = new DMesh(); + BlendMeshBuilder builder = new BlendMeshBuilder(mesh); + builder.setOffset(offset); + builder.setWellBehaved(true); + //#if MC>=10809 + builder.begin(7, DefaultVertexFormats.PARTICLE_POSITION_TEX_COLOR_LMAP); + //#else + //$$ builder.startDrawingQuads(); + //#endif + //#if MC>=10809 + particle.renderParticle(builder, MCVer.getMinecraft().getRenderViewEntity(), 0, 1, 1, 0, 0, 0); + //#else + //$$ particle.func_180434_a(builder, Minecraft.getMinecraft().getRenderViewEntity(), 0, 1, 1, 0, 0, 0); + //#endif + builder.finishDrawing(); + return mesh; + } + + @Override + public void postFrame(int frame) throws IOException { + particleObjects.entrySet().stream() + .filter(entry -> !particleObjectsSeen.containsKey(entry.getKey())) + .forEach(entry -> entry.getValue().keyframe("hide", 0, renderState.getFrame(), 1f)); + particleObjects = particleObjectsSeen; + particleObjectsSeen = new IdentityHashMap<>(); + } +} +//#endif 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..9f385ce3 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/RenderState.java @@ -0,0 +1,145 @@ +//#if MC>=10800 +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 de.johni0702.minecraft.gui.utils.lwjgl.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 { + + } +} +//#endif 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..b6fd2c59 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/exporters/TileEntityExporter.java @@ -0,0 +1,111 @@ +//#if MC>=10800 +package com.replaymod.render.blend.exporters; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.Exporter; +import com.replaymod.render.blend.data.DObject; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Matrix4f; +import de.johni0702.minecraft.gui.utils.lwjgl.vector.Vector3f; +import lombok.SneakyThrows; +import net.minecraft.client.Minecraft; +import net.minecraft.tileentity.TileEntity; + +//#if MC>=10904 +import net.minecraft.util.math.BlockPos; +//#else +//$$ import net.minecraft.util.BlockPos; +//#endif + +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 = MCVer.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, float alpha) { + // FIXME: handle alpha + 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<>(); + } +} +//#endif 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..46ea9cde --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinChunkRenderWorker.java @@ -0,0 +1,44 @@ +//#if MC>=10800 +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.ChunkRenderWorker; +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; + +//#if MC>=11300 +import net.minecraft.client.renderer.chunk.ChunkRenderTask; +//#else +//$$ import net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator; +//#endif + +@Mixin(ChunkRenderWorker.class) +public abstract class MixinChunkRenderWorker { + + @Inject(method = "processTask", at = @At("RETURN")) + //#if MC>=11300 + public void afterChunkUpdate(ChunkRenderTask task, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + if (task.getStatus() == ChunkRenderTask.Status.DONE + && task.getType() == ChunkRenderTask.Type.REBUILD_CHUNK) { + blendState.get(ChunkExporter.class).addChunkUpdate(task.getRenderChunk(), task.getCompiledChunk()); + } + } + } + //#else + //$$ 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) { + //$$ blendState.get(ChunkExporter.class).addChunkUpdate(task.getRenderChunk(), task.getCompiledChunk()); + //$$ } + //$$ } + //$$ } + //#endif +} +//#endif 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..2a78b54c --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinEffectRenderer.java @@ -0,0 +1,57 @@ +//#if MC>=10800 +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ParticlesExporter; +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; + +//#if MC>=10904 +import net.minecraft.client.particle.ParticleManager; +//#else +//$$ import net.minecraft.client.particle.EffectRenderer; +//#endif + +//#if MC>=10904 +@Mixin(ParticleManager.class) +//#else +//$$ @Mixin(EffectRenderer.class) +//#endif +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(); + } + } +} +//#endif 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..e452b1f5 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinModelRenderer.java @@ -0,0 +1,72 @@ +//#if MC>=10800 +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ModelRendererExporter; +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; + +//#if MC>=11300 +import net.minecraft.client.renderer.entity.model.ModelRenderer; +//#else +//$$ import net.minecraft.client.model.ModelRenderer; +//#endif + +@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(); + } + } +} +//#endif 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..bd1384b2 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderGlobal.java @@ -0,0 +1,89 @@ +//#if MC>=10800 +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.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; + +//#if MC>=11300 +import net.minecraft.client.renderer.WorldRenderer; +//#else +//$$ import net.minecraft.client.renderer.RenderGlobal; +//#endif + +//#if MC>=11300 +@Mixin(WorldRenderer.class) +//#else +//$$ @Mixin(RenderGlobal.class) +//#endif +public abstract class MixinRenderGlobal { + + // FIXME wither skull ._. mojang pls + + @Inject(method = "renderEntities", + at = @At(value = "INVOKE", + //#if MC>=10904 + target = "Lnet/minecraft/client/renderer/entity/RenderManager;renderEntityStatic(Lnet/minecraft/entity/Entity;FZ)V")) + //#else + //$$ target = "Lnet/minecraft/client/renderer/entity/RenderManager;renderEntitySimple(Lnet/minecraft/entity/Entity;F)Z")) + //#endif + 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", + //#if MC>=10904 + target = "Lnet/minecraft/client/renderer/entity/RenderManager;renderEntityStatic(Lnet/minecraft/entity/Entity;FZ)V", + //#else + //$$ target = "Lnet/minecraft/client/renderer/entity/RenderManager;renderEntitySimple(Lnet/minecraft/entity/Entity;F)Z", + //#endif + 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", + //#if MC>=11300 + target = "Lnet/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher;render(Lnet/minecraft/tileentity/TileEntity;FI)V" + //#else + //$$ target = "Lnet/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher;renderTileEntity(Lnet/minecraft/tileentity/TileEntity;FI)V" + //#endif + )) + 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", + //#if MC>=11300 + target = "Lnet/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher;render(Lnet/minecraft/tileentity/TileEntity;FI)V", + //#else + //$$ target = "Lnet/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher;renderTileEntity(Lnet/minecraft/tileentity/TileEntity;FI)V", + //#endif + 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(); + } + } +} +//#endif 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..016695b3 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderItem.java @@ -0,0 +1,50 @@ +//#if MC>=10800 +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ItemExporter; +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; + +//#if MC>=11300 +import net.minecraft.client.renderer.ItemRenderer; +import net.minecraft.client.renderer.model.IBakedModel; +//#else +//#if MC>=10904 +//$$ import net.minecraft.client.renderer.RenderItem; +//$$ import net.minecraft.client.renderer.block.model.IBakedModel; +//#else +//$$ import net.minecraft.client.renderer.entity.RenderItem; +//$$ import net.minecraft.client.resources.model.IBakedModel; +//#endif +//#endif + +//#if MC>=11300 +@Mixin(ItemRenderer.class) +//#else +//$$ @Mixin(RenderItem.class) +//#endif +public abstract class MixinRenderItem { + //#if MC>=11300 + @Inject(method = "renderModel(Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/item/ItemStack;)V", + at = @At("HEAD")) + //#else + //#if MC>=10904 + //$$ @Inject(method = "renderModel(Lnet/minecraft/client/renderer/block/model/IBakedModel;Lnet/minecraft/item/ItemStack;)V", + //$$ at = @At("HEAD")) + //#else + //$$ @Inject(method = "renderModel(Lnet/minecraft/client/resources/model/IBakedModel;Lnet/minecraft/item/ItemStack;)V", + //$$ at = @At("HEAD")) + //#endif + //#endif + private void onRenderModel(IBakedModel model, ItemStack stack, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ItemExporter.class).onRender(this, model, stack); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/render/blend/mixin/MixinRenderLivingBase.java b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderLivingBase.java new file mode 100644 index 00000000..a470219a --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderLivingBase.java @@ -0,0 +1,40 @@ +//#if MC>=10800 +package com.replaymod.render.blend.mixin; + +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.EntityExporter; +import net.minecraft.entity.EntityLivingBase; +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; + +//#if MC>=10904 +import net.minecraft.client.renderer.entity.RenderLivingBase; +//#else +//$$ import net.minecraft.client.renderer.entity.RendererLivingEntity; +//#endif + +//#if MC>=10904 +@Mixin(RenderLivingBase.class) +//#else +//$$ @Mixin(RendererLivingEntity.class) +//#endif +public abstract class MixinRenderLivingBase { + @Inject(method = "doRender", at = @At( + value = "INVOKE", + //#if MC>=10904 + target = "Lnet/minecraft/client/renderer/entity/RenderLivingBase;prepareScale(Lnet/minecraft/entity/EntityLivingBase;F)F", + //#else + //$$ target = "Lnet/minecraft/client/renderer/entity/RendererLivingEntity;preRenderCallback(Lnet/minecraft/entity/EntityLivingBase;F)V", + //#endif + shift = At.Shift.AFTER + )) + private void recordModelMatrix(EntityLivingBase entity, double x, double y, double z, float entityYaw, float partialTicks, CallbackInfo ci) { + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(EntityExporter.class).postEntityLivingSetup(); + } + } +} +//#endif 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..17931095 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinRenderManager.java @@ -0,0 +1,62 @@ +//#if MC>=10800 +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; + +//#if MC>=10904 +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#else +//$$ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +//#endif + +@Mixin(RenderManager.class) +public abstract class MixinRenderManager { + + @Inject( + //#if MC>=11300 + method = "renderEntity", + //#else + //$$ method = "doRenderEntity", + //#endif + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/entity/Render;doRender(Lnet/minecraft/entity/Entity;DDDFF)V")) + public void preRender(Entity entity, double x, double y, double z, float yaw, float renderPartialTicks, boolean box, + //#if MC>=10904 + CallbackInfo ci) { + //#else + //$$ CallbackInfoReturnable ci) { + //#endif + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(EntityExporter.class).preRender(entity, x, y, z, yaw, renderPartialTicks); + } + } + + @Inject( + //#if MC>=11300 + method = "renderEntity", + //#else + //$$ method = "doRenderEntity", + //#endif + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/entity/Render;doRender(Lnet/minecraft/entity/Entity;DDDFF)V", + shift = At.Shift.AFTER)) + public void postRender(Entity entity, double x, double y, double z, float yaw, float renderPartialTicks, boolean box, + //#if MC>=10904 + CallbackInfo ci) { + //#else + //$$ CallbackInfoReturnable ci) { + //#endif + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(EntityExporter.class).postRender(entity, x, y, z, yaw, renderPartialTicks); + } + } +} +//#endif 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..b0688fa0 --- /dev/null +++ b/src/main/java/com/replaymod/render/blend/mixin/MixinTileEntityRendererDispatcher.java @@ -0,0 +1,60 @@ +//#if MC>=10800 +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 { + + //#if MC>=11300 + @Inject(method = "render(Lnet/minecraft/tileentity/TileEntity;DDDFIZ)V", + at = @At("HEAD")) + public void preRender(TileEntity tileEntity, double x, double y, double z, float renderPartialTicks, int destroyStage, boolean hasNoBlock, CallbackInfo ci) { + float alpha = 1; + //#else + //#if MC>=11200 + //$$ @Inject(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDFIF)V", + //$$ at = @At("HEAD")) + //$$ public void preRender(TileEntity tileEntity, double x, double y, double z, float renderPartialTicks, int destroyStage, float alpha, CallbackInfo ci) { + //#else + //$$ @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) { + //$$ float alpha = 1; + //#endif + //#endif + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(TileEntityExporter.class).preRender(tileEntity, x, y, z, renderPartialTicks, destroyStage, alpha); + } + } + + //#if MC>=11300 + @Inject(method = "render(Lnet/minecraft/tileentity/TileEntity;DDDFIZ)V", + at = @At("RETURN")) + public void postRender(TileEntity tileEntity, double x, double y, double z, float renderPartialTicks, int destroyStage, boolean hasNoBlock, CallbackInfo ci) { + //#else + //#if MC>=11200 + //$$ @Inject(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDFIF)V", + //$$ at = @At("RETURN")) + //$$ public void postRender(TileEntity tileEntity, double x, double y, double z, float renderPartialTicks, int destroyStage, float alpha, CallbackInfo ci) { + //#else + //$$ @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) { + //#endif + //#endif + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(TileEntityExporter.class).postRender(); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index b5e378ab..15d23d9c 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -59,6 +59,14 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { new GuiDropdownMenu().onSelection(new Consumer() { @Override public void consume(Integer old) { + if (renderMethodDropdown.getSelectedValue() == RenderSettings.RenderMethod.BLEND + ^ encodingPresetDropdown.getSelectedValue() == RenderSettings.EncodingPreset.BLEND) { + if (renderMethodDropdown.getSelectedValue() == RenderSettings.RenderMethod.BLEND) { + encodingPresetDropdown.setSelected(RenderSettings.EncodingPreset.BLEND); + } else { + encodingPresetDropdown.setSelected(RenderSettings.EncodingPreset.MP4_DEFAULT); + } + } updateInputs(); } }).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.RenderMethod.values()); @@ -75,6 +83,10 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { @Override public void consume(Integer old) { RenderSettings.EncodingPreset newPreset = encodingPresetDropdown.getSelectedValue(); + if (newPreset == RenderSettings.EncodingPreset.BLEND + && encodingPresetDropdown.getSelectedValue() == RenderSettings.EncodingPreset.BLEND) { + renderMethodDropdown.setSelected(RenderSettings.RenderMethod.BLEND); + } // Update export arguments to match new Preset exportArguments.setText(newPreset.getValue()); // If the user hasn't changed the output file by themselves, @@ -361,6 +373,17 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { injectSphericalMetadata.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED) .setI18nText("replaymod.gui.rendersettings.sphericalmetadata.error")); } + + // Enable/Disable various options for blend export + boolean isBlend = renderMethod == RenderSettings.RenderMethod.BLEND; + if (isBlend) { + videoWidth.setDisabled(); + videoHeight.setDisabled(); + } + encodingPresetDropdown.setEnabled(!isBlend); + exportCommand.setEnabled(!isBlend); + exportArguments.setEnabled(!isBlend); + antiAliasingDropdown.setEnabled(!isBlend); } protected String updateResolution() { @@ -400,6 +423,12 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } } + //#if MC<10800 + //$$ if (method == RenderSettings.RenderMethod.BLEND) { + //$$ return "replaymod.gui.rendersettings.no_blend_on_1_7_10"; + //$$ } + //#endif + return null; } diff --git a/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java b/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java index f9d8587c..9a9269f8 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java +++ b/src/main/java/com/replaymod/render/mixin/MixinEffectRenderer.java @@ -1,6 +1,7 @@ package com.replaymod.render.mixin; //#if MC<10904 +//$$ import com.replaymod.render.blend.BlendState; //$$ import com.replaymod.render.hooks.EntityRendererHandler; //$$ import net.minecraft.client.Minecraft; //$$ import net.minecraft.client.particle.EffectRenderer; @@ -11,6 +12,7 @@ package com.replaymod.render.mixin; //$$ import org.spongepowered.asm.mixin.injection.Redirect; //$$ //#if MC>=10800 +//$$ import com.replaymod.render.blend.exporters.ParticlesExporter; //$$ import net.minecraft.client.renderer.WorldRenderer; //#else //$$ import net.minecraft.client.renderer.Tessellator; @@ -81,6 +83,12 @@ package com.replaymod.render.mixin; //$$ rotYZ = (float) (-rotZ * Math.sin(pitch)); //$$ rotXY = (float) (rotX * Math.sin(pitch)); //$$ } + //#if MC>=10800 + //$$ BlendState blendState = BlendState.getState(); + //$$ if (blendState != null) { + //$$ blendState.get(ParticlesExporter.class).onRender(fx, partialTicks); + //$$ } + //#endif //#if MC>=10809 //$$ fx.renderParticle(worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY); //#else diff --git a/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java b/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java index 87fed192..8ce3cf60 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java +++ b/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java @@ -2,6 +2,8 @@ package com.replaymod.render.mixin; //#if MC>=10904 import com.replaymod.core.versions.MCVer; +import com.replaymod.render.blend.BlendState; +import com.replaymod.render.blend.exporters.ParticlesExporter; import com.replaymod.render.hooks.EntityRendererHandler; import net.minecraft.client.particle.Particle; import net.minecraft.client.particle.ParticleManager; @@ -62,6 +64,10 @@ public abstract class MixinParticleManager { rotYZ = (float) (-rotZ * Math.sin(pitch)); rotXY = (float) (rotX * Math.sin(pitch)); } + BlendState blendState = BlendState.getState(); + if (blendState != null) { + blendState.get(ParticlesExporter.class).onRender(particle, partialTicks); + } particle.renderParticle(vertexBuffer, 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 d2e1b236..89b0585b 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); } @@ -84,4 +105,19 @@ public class Pipelines { new ODSFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, processor.getFrameSize()); return new Pipeline<>(capturer, processor, consumer); } + + public static Pipeline newBlendPipeline(RenderInfo renderInfo) { + RenderSettings settings = renderInfo.getRenderSettings(); + FrameCapturer capturer = new BlendFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo); + FrameConsumer consumer = new FrameConsumer() { + @Override + public void consume(RGBFrame frame) { + } + + @Override + public void close() { + } + }; + 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 f886624e..b338f9c9 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -8,6 +8,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; @@ -100,14 +101,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); + } + }); + } } /** @@ -353,7 +361,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) { @@ -531,7 +539,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/META-INF/replaymod_at.cfg b/src/main/resources/META-INF/replaymod_at.cfg index ce8621b7..0df2108e 100644 --- a/src/main/resources/META-INF/replaymod_at.cfg +++ b/src/main/resources/META-INF/replaymod_at.cfg @@ -110,8 +110,11 @@ public net.minecraft.client.renderer.WorldRenderer field_72740_G # renderEntitie ##endif ##endif -public net.minecraft.client.renderer.RenderGlobal$ContainerLocalRenderInformation -#public net.minecraft.client.renderer.RenderGlobal$ContainerLocalRenderInformation field_178553_a # renderChunk +##if MC>=11300 +public net.minecraft.client.renderer.WorldRenderer$ContainerLocalRenderInformation +##else +#$$ public net.minecraft.client.renderer.RenderGlobal$ContainerLocalRenderInformation +##endif # ChunkRenderDispatcher public-f net.minecraft.client.renderer.chunk.ChunkRenderDispatcher field_178519_d # queueChunkUpdates @@ -128,6 +131,11 @@ public net.minecraft.client.renderer.entity.RenderManager field_178636_l # skinM public net.minecraft.client.renderer.ItemRenderer * public net.minecraft.client.renderer.ItemRenderer *() +# RenderItem +##if MC>=10904 +public net.minecraft.client.renderer.RenderItem field_184395_f # itemColors +##endif + # EntityPlayer public net.minecraft.entity.player.EntityPlayer * diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 37de62cf..874fd801 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 37de62cf8edae480b2a78cb6f5ed307aec351a69 +Subproject commit 874fd8016db8209e6a4e8822c435c9641bcc18f6 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..f4eb4a6a --- /dev/null +++ b/src/main/resources/mixins.render.blend.replaymod.json @@ -0,0 +1,21 @@ +{ + "required": true, + "package": "com.replaymod.render.blend.mixin", + "mixins": [], + "server": [], + "client": [ + //#if MC>=10800 + "MixinChunkRenderWorker", + "MixinEffectRenderer", + "MixinModelRenderer", + "MixinRenderGlobal", + "MixinRenderItem", + "MixinRenderLivingBase", + "MixinRenderManager", + "MixinTileEntityRendererDispatcher" + //#endif + ], + "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 d17913ac..0b90624c 100644 --- a/versions/common.gradle +++ b/versions/common.gradle @@ -179,6 +179,8 @@ dependencies { shade 'org.aspectj:aspectjrt:1.8.2' + shade 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8' + def studioVersion = mcVersionStr if (studioVersion == '1.8.9') studioVersion = '1.8' shade "com.github.ReplayMod.ReplayStudio:$studioVersion:c41d016", shadeExclusions @@ -190,6 +192,9 @@ dependencies { transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those exclude group: 'org.projectlombok', module: 'lombok' // runtime only for @SneakyThrows which isn't used } + project(":jGui:$jGuiVersion").afterEvaluate { + shade files(project(":jGui:$jGuiVersion").tasks.preshadowJar.archivePath) // include pre-shadowed lwjgl_util + } if (!gradle.gradleVersion.startsWith('3.')) { annotationProcessor 'org.projectlombok:lombok:1.16.6'