/* * Minecraft Forge * Copyright (c) 2016-2019. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package net.minecraftforge.client; import javax.annotation.Nonnull; import java.nio.ByteBuffer; import java.util.function.Predicate; import org.lwjgl.opengl.GL11; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.WorldClient; import net.minecraft.client.renderer.BufferBuilder; import net.minecraft.client.renderer.GLAllocation; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.client.renderer.vertex.VertexBuffer; import net.minecraft.client.renderer.vertex.VertexFormat; import net.minecraft.entity.Entity; import net.minecraft.resources.IReloadableResourceManager; import net.minecraft.resources.IResourceManager; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; import net.minecraftforge.common.ForgeConfig; import net.minecraftforge.resource.IResourceType; import net.minecraftforge.resource.ISelectiveResourceReloadListener; import net.minecraftforge.resource.VanillaResourceType; public class CloudRenderer implements ISelectiveResourceReloadListener { // Shared constants. private static final float PX_SIZE = 1 / 256F; // Building constants. private static final VertexFormat FORMAT = DefaultVertexFormats.POSITION_TEX_COLOR; private static final int TOP_SECTIONS = 12; // Number of slices a top face will span. private static final int HEIGHT = 4; private static final float INSET = 0.001F; private static final float ALPHA = 0.8F; // Debug private static final boolean WIREFRAME = false; // Instance fields private final Minecraft mc = Minecraft.getInstance(); private final ResourceLocation texture = new ResourceLocation("textures/environment/clouds.png"); private int displayList = -1; private VertexBuffer vbo; private int cloudMode = -1; private int renderDistance = -1; private DynamicTexture COLOR_TEX = null; private int texW; private int texH; public CloudRenderer() { // Resource manager should always be reloadable. ((IReloadableResourceManager) mc.getResourceManager()).addReloadListener(this); } private int getScale() { return cloudMode == 2 ? 12 : 8; } private float ceilToScale(float value) { float scale = getScale(); return MathHelper.ceil(value / scale) * scale; } private void vertices(BufferBuilder buffer) { boolean fancy = cloudMode == 2; // Defines whether to hide all but the bottom. float scale = getScale(); float CULL_DIST = 2 * scale; float bCol = fancy ? 0.7F : 1F; float sectEnd = ceilToScale((renderDistance * 2) * 16); float sectStart = -sectEnd; float sectStep = ceilToScale(sectEnd * 2 / TOP_SECTIONS); float sectPx = PX_SIZE / scale; buffer.begin(GL11.GL_QUADS, FORMAT); float sectX0 = sectStart; float sectX1 = sectX0; while (sectX1 < sectEnd) { sectX1 += sectStep; if (sectX1 > sectEnd) sectX1 = sectEnd; float sectZ0 = sectStart; float sectZ1 = sectZ0; while (sectZ1 < sectEnd) { sectZ1 += sectStep; if (sectZ1 > sectEnd) sectZ1 = sectEnd; float u0 = sectX0 * sectPx; float u1 = sectX1 * sectPx; float v0 = sectZ0 * sectPx; float v1 = sectZ1 * sectPx; // Bottom buffer.pos(sectX0, 0, sectZ0).tex(u0, v0).color(bCol, bCol, bCol, ALPHA).endVertex(); buffer.pos(sectX1, 0, sectZ0).tex(u1, v0).color(bCol, bCol, bCol, ALPHA).endVertex(); buffer.pos(sectX1, 0, sectZ1).tex(u1, v1).color(bCol, bCol, bCol, ALPHA).endVertex(); buffer.pos(sectX0, 0, sectZ1).tex(u0, v1).color(bCol, bCol, bCol, ALPHA).endVertex(); if (fancy) { // Top buffer.pos(sectX0, HEIGHT, sectZ0).tex(u0, v0).color(1, 1, 1, ALPHA).endVertex(); buffer.pos(sectX0, HEIGHT, sectZ1).tex(u0, v1).color(1, 1, 1, ALPHA).endVertex(); buffer.pos(sectX1, HEIGHT, sectZ1).tex(u1, v1).color(1, 1, 1, ALPHA).endVertex(); buffer.pos(sectX1, HEIGHT, sectZ0).tex(u1, v0).color(1, 1, 1, ALPHA).endVertex(); float slice; float sliceCoord0; float sliceCoord1; for (slice = sectX0; slice < sectX1;) { sliceCoord0 = slice * sectPx; sliceCoord1 = sliceCoord0 + PX_SIZE; // X sides if (slice > -CULL_DIST) { slice += INSET; buffer.pos(slice, 0, sectZ1).tex(sliceCoord0, v1).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); buffer.pos(slice, HEIGHT, sectZ1).tex(sliceCoord1, v1).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); buffer.pos(slice, HEIGHT, sectZ0).tex(sliceCoord1, v0).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); buffer.pos(slice, 0, sectZ0).tex(sliceCoord0, v0).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); slice -= INSET; } slice += scale; if (slice <= CULL_DIST) { slice -= INSET; buffer.pos(slice, 0, sectZ0).tex(sliceCoord0, v0).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); buffer.pos(slice, HEIGHT, sectZ0).tex(sliceCoord1, v0).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); buffer.pos(slice, HEIGHT, sectZ1).tex(sliceCoord1, v1).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); buffer.pos(slice, 0, sectZ1).tex(sliceCoord0, v1).color(0.9F, 0.9F, 0.9F, ALPHA).endVertex(); slice += INSET; } } for (slice = sectZ0; slice < sectZ1;) { sliceCoord0 = slice * sectPx; sliceCoord1 = sliceCoord0 + PX_SIZE; // Z sides if (slice > -CULL_DIST) { slice += INSET; buffer.pos(sectX0, 0, slice).tex(u0, sliceCoord0).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); buffer.pos(sectX0, HEIGHT, slice).tex(u0, sliceCoord1).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); buffer.pos(sectX1, HEIGHT, slice).tex(u1, sliceCoord1).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); buffer.pos(sectX1, 0, slice).tex(u1, sliceCoord0).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); slice -= INSET; } slice += scale; if (slice <= CULL_DIST) { slice -= INSET; buffer.pos(sectX1, 0, slice).tex(u1, sliceCoord0).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); buffer.pos(sectX1, HEIGHT, slice).tex(u1, sliceCoord1).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); buffer.pos(sectX0, HEIGHT, slice).tex(u0, sliceCoord1).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); buffer.pos(sectX0, 0, slice).tex(u0, sliceCoord0).color(0.8F, 0.8F, 0.8F, ALPHA).endVertex(); slice += INSET; } } } sectZ0 = sectZ1; } sectX0 = sectX1; } } private void dispose() { if (vbo != null) { vbo.deleteGlBuffers(); vbo = null; } if (displayList >= 0) { GLAllocation.deleteDisplayLists(displayList); displayList = -1; } } private void build() { Tessellator tess = Tessellator.getInstance(); BufferBuilder buffer = tess.getBuffer(); if (OpenGlHelper.useVbo()) vbo = new VertexBuffer(FORMAT); else GlStateManager.newList(displayList = GLAllocation.generateDisplayLists(1), GL11.GL_COMPILE); vertices(buffer); if (OpenGlHelper.useVbo()) { buffer.finishDrawing(); buffer.reset(); vbo.bufferData(buffer.getByteBuffer()); } else { tess.draw(); GlStateManager.endList(); } } private int fullCoord(double coord, int scale) { // Corrects misalignment of UV offset when on negative coords. return ((int) coord / scale) - (coord < 0 ? 1 : 0); } private boolean isBuilt() { return OpenGlHelper.useVbo() ? vbo != null : displayList >= 0; } public void checkSettings() { boolean newEnabled = ForgeConfig.CLIENT.forgeCloudsEnabled.get() && mc.gameSettings.shouldRenderClouds() != 0 && mc.world != null && mc.world.dimension.isSurfaceWorld(); if (isBuilt() && (!newEnabled || mc.gameSettings.shouldRenderClouds() != cloudMode || mc.gameSettings.renderDistanceChunks != renderDistance)) { dispose(); } cloudMode = mc.gameSettings.shouldRenderClouds(); renderDistance = mc.gameSettings.renderDistanceChunks; if (newEnabled && !isBuilt()) { build(); } } public boolean render(int cloudTicks, float partialTicks) { if (!isBuilt()) return false; Entity entity = mc.getRenderViewEntity(); double totalOffset = cloudTicks + partialTicks; double x = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks + totalOffset * 0.03; double y = mc.world.dimension.getCloudHeight() - (entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks) + 0.33; double z = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks; int scale = getScale(); if (cloudMode == 2) z += 0.33 * scale; // Integer UVs to translate the texture matrix by. int offU = fullCoord(x, scale); int offV = fullCoord(z, scale); GlStateManager.pushMatrix(); // Translate by the remainder after the UV offset. GlStateManager.translated((offU * scale) - x, y, (offV * scale) - z); // Modulo to prevent texture samples becoming inaccurate at extreme offsets. offU = offU % texW; offV = offV % texH; // Translate the texture. GlStateManager.matrixMode(GL11.GL_TEXTURE); GlStateManager.translatef(offU * PX_SIZE, offV * PX_SIZE, 0); GlStateManager.matrixMode(GL11.GL_MODELVIEW); GlStateManager.disableCull(); GlStateManager.enableBlend(); GlStateManager.blendFuncSeparate( GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); // Color multiplier. Vec3d color = mc.world.getCloudColour(partialTicks); float r = (float) color.x; float g = (float) color.y; float b = (float) color.z; if (COLOR_TEX == null) COLOR_TEX = new DynamicTexture(1, 1, false); // Apply a color multiplier through a texture upload if shaders aren't supported. COLOR_TEX.getTextureData().setPixelRGBA(0, 0, 255 << 24 | ((int) (r * 255)) << 16 | ((int) (g * 255)) << 8 | (int) (b * 255)); COLOR_TEX.updateDynamicTexture(); GlStateManager.activeTexture(OpenGlHelper.GL_TEXTURE1); GlStateManager.bindTexture(COLOR_TEX.getGlTextureId()); GlStateManager.enableTexture2D(); // Bind the clouds texture last so the shader's sampler2D is correct. GlStateManager.activeTexture(OpenGlHelper.GL_TEXTURE0); mc.textureManager.bindTexture(texture); ByteBuffer buffer = Tessellator.getInstance().getBuffer().getByteBuffer(); // Set up pointers for the display list/VBO. if (OpenGlHelper.useVbo()) { vbo.bindBuffer(); int stride = FORMAT.getSize(); GlStateManager.vertexPointer(3, GL11.GL_FLOAT, stride, 0); GlStateManager.enableClientState(GL11.GL_VERTEX_ARRAY); GlStateManager.texCoordPointer(2, GL11.GL_FLOAT, stride, 12); GlStateManager.enableClientState(GL11.GL_TEXTURE_COORD_ARRAY); GlStateManager.colorPointer(4, GL11.GL_UNSIGNED_BYTE, stride, 20); GlStateManager.enableClientState(GL11.GL_COLOR_ARRAY); } else { buffer.limit(FORMAT.getSize()); for (int i = 0; i < FORMAT.getElementCount(); i++) FORMAT.getElements().get(i).getUsage().preDraw(FORMAT, i, FORMAT.getSize(), buffer); buffer.position(0); } // Depth pass to prevent insides rendering from the outside. GlStateManager.colorMask(false, false, false, false); if (OpenGlHelper.useVbo()) vbo.drawArrays(GL11.GL_QUADS); else GlStateManager.callList(displayList); // Full render. GlStateManager.colorMask(true, true, true, true); // Wireframe for debug. if (WIREFRAME) { GlStateManager.polygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE); GlStateManager.lineWidth(2.0F); GlStateManager.disableTexture2D(); GlStateManager.depthMask(false); GlStateManager.disableFog(); if (OpenGlHelper.useVbo()) vbo.drawArrays(GL11.GL_QUADS); else GlStateManager.callList(displayList); GlStateManager.polygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL); GlStateManager.depthMask(true); GlStateManager.enableTexture2D(); GlStateManager.enableFog(); } if (OpenGlHelper.useVbo()) { vbo.drawArrays(GL11.GL_QUADS); vbo.unbindBuffer(); // Unbind buffer and disable pointers. } else { GlStateManager.callList(displayList); } buffer.limit(0); for (int i = 0; i < FORMAT.getElementCount(); i++) FORMAT.getElements().get(i).getUsage().postDraw(FORMAT, i, FORMAT.getSize(), buffer); buffer.position(0); // Disable our coloring. GlStateManager.activeTexture(OpenGlHelper.GL_TEXTURE1); GlStateManager.disableTexture2D(); GlStateManager.activeTexture(OpenGlHelper.GL_TEXTURE0); // Reset texture matrix. GlStateManager.matrixMode(GL11.GL_TEXTURE); GlStateManager.loadIdentity(); GlStateManager.matrixMode(GL11.GL_MODELVIEW); GlStateManager.disableBlend(); GlStateManager.enableCull(); GlStateManager.popMatrix(); return true; } private void reloadTextures() { if (mc.textureManager != null) { mc.textureManager.bindTexture(texture); texW = GlStateManager.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_WIDTH); texH = GlStateManager.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_HEIGHT); } } @Override public void onResourceManagerReload(@Nonnull IResourceManager resourceManager, @Nonnull Predicate resourcePredicate) { if (resourcePredicate.test(VanillaResourceType.TEXTURES)) { reloadTextures(); } } private static CloudRenderer cloudRenderer; private static CloudRenderer getCloudRenderer() { if (cloudRenderer == null) cloudRenderer = new CloudRenderer(); return cloudRenderer; } public static void updateCloudSettings() { getCloudRenderer().checkSettings(); } public static boolean renderClouds(int cloudTicks, float partialTicks, WorldClient world, Minecraft client) { IRenderHandler renderer = world.dimension.getCloudRenderer(); if (renderer != null) { renderer.render(partialTicks, world, client); return true; } return getCloudRenderer().render(cloudTicks, partialTicks); } }