package net.minecraft.client.renderer; import com.mojang.blaze3d.buffers.GpuBuffer; import com.mojang.blaze3d.buffers.GpuBufferSlice; import com.mojang.blaze3d.buffers.Std140Builder; import com.mojang.blaze3d.buffers.Std140SizeCalculator; import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.pipeline.RenderTarget; import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.systems.RenderPass; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.GpuTextureView; import com.mojang.blaze3d.vertex.VertexFormat; import com.mojang.logging.LogUtils; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.CloudStatus; import net.minecraft.client.Minecraft; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.SimplePreparableReloadListener; import net.minecraft.util.ARGB; import net.minecraft.util.Mth; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; import org.joml.Vector3f; import org.joml.Vector4f; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class CloudRenderer extends SimplePreparableReloadListener> implements AutoCloseable { private static final int FLAG_INSIDE_FACE = 16; private static final int FLAG_USE_TOP_COLOR = 32; private static final int MAX_RADIUS_CHUNKS = 128; private static final float CELL_SIZE_IN_BLOCKS = 12.0F; private static final int UBO_SIZE = new Std140SizeCalculator().putVec4().putVec3().putVec3().get(); private static final Logger LOGGER = LogUtils.getLogger(); private static final ResourceLocation TEXTURE_LOCATION = ResourceLocation.withDefaultNamespace("textures/environment/clouds.png"); private static final float BLOCKS_PER_SECOND = 0.6F; private static final long EMPTY_CELL = 0L; private static final int COLOR_OFFSET = 4; private static final int NORTH_OFFSET = 3; private static final int EAST_OFFSET = 2; private static final int SOUTH_OFFSET = 1; private static final int WEST_OFFSET = 0; private boolean needsRebuild = true; private int prevCellX = Integer.MIN_VALUE; private int prevCellZ = Integer.MIN_VALUE; private CloudRenderer.RelativeCameraPos prevRelativeCameraPos = CloudRenderer.RelativeCameraPos.INSIDE_CLOUDS; @Nullable private CloudStatus prevType; @Nullable private CloudRenderer.TextureData texture; private int quadCount = 0; private final RenderSystem.AutoStorageIndexBuffer indices = RenderSystem.getSequentialBuffer(VertexFormat.Mode.QUADS); private final MappableRingBuffer ubo = new MappableRingBuffer(() -> "Cloud UBO", 130, UBO_SIZE); @Nullable private MappableRingBuffer utb; /** * Performs any reloading that can be done off-thread, such as file IO */ protected Optional prepare(ResourceManager resourceManager, ProfilerFiller profiler) { try { InputStream inputStream = resourceManager.open(TEXTURE_LOCATION); Optional var20; try (NativeImage nativeImage = NativeImage.read(inputStream)) { int i = nativeImage.getWidth(); int j = nativeImage.getHeight(); long[] ls = new long[i * j]; for (int k = 0; k < j; k++) { for (int l = 0; l < i; l++) { int m = nativeImage.getPixel(l, k); if (isCellEmpty(m)) { ls[l + k * i] = 0L; } else { boolean bl = isCellEmpty(nativeImage.getPixel(l, Math.floorMod(k - 1, j))); boolean bl2 = isCellEmpty(nativeImage.getPixel(Math.floorMod(l + 1, j), k)); boolean bl3 = isCellEmpty(nativeImage.getPixel(l, Math.floorMod(k + 1, j))); boolean bl4 = isCellEmpty(nativeImage.getPixel(Math.floorMod(l - 1, j), k)); ls[l + k * i] = packCellData(m, bl, bl2, bl3, bl4); } } } var20 = Optional.of(new CloudRenderer.TextureData(ls, i, j)); } catch (Throwable var18) { if (inputStream != null) { try { inputStream.close(); } catch (Throwable var15) { var18.addSuppressed(var15); } } throw var18; } if (inputStream != null) { inputStream.close(); } return var20; } catch (IOException var19) { LOGGER.error("Failed to load cloud texture", (Throwable)var19); return Optional.empty(); } } private static int getSizeForCloudDistance(int cloudDistance) { int i = 4; int j = (cloudDistance + 1) * 2 * (cloudDistance + 1) * 2 / 2; int k = j * 4 + 54; return k * 3; } protected void apply(Optional object, ResourceManager resourceManager, ProfilerFiller profiler) { this.texture = (CloudRenderer.TextureData)object.orElse(null); this.needsRebuild = true; } private static boolean isCellEmpty(int color) { return ARGB.alpha(color) < 10; } private static long packCellData(int color, boolean northEmpty, boolean eastEmpty, boolean southEmpty, boolean westEmpty) { return (long)color << 4 | (northEmpty ? 1 : 0) << 3 | (eastEmpty ? 1 : 0) << 2 | (southEmpty ? 1 : 0) << 1 | (westEmpty ? 1 : 0) << 0; } private static boolean isNorthEmpty(long cellData) { return (cellData >> 3 & 1L) != 0L; } private static boolean isEastEmpty(long cellData) { return (cellData >> 2 & 1L) != 0L; } private static boolean isSouthEmpty(long cellData) { return (cellData >> 1 & 1L) != 0L; } private static boolean isWestEmpty(long cellData) { return (cellData >> 0 & 1L) != 0L; } public void render(int cloudColor, CloudStatus cloudStatus, float height, Vec3 cameraPosition, float ticks) { if (this.texture != null) { int i = Math.min(Minecraft.getInstance().options.cloudRange().get(), 128) * 16; int j = Mth.ceil(i / 12.0F); int k = getSizeForCloudDistance(j); if (this.utb == null || this.utb.currentBuffer().size() != k) { if (this.utb != null) { this.utb.close(); } this.utb = new MappableRingBuffer(() -> "Cloud UTB", 258, k); } float f = (float)(height - cameraPosition.y); float g = f + 4.0F; CloudRenderer.RelativeCameraPos relativeCameraPos; if (g < 0.0F) { relativeCameraPos = CloudRenderer.RelativeCameraPos.ABOVE_CLOUDS; } else if (f > 0.0F) { relativeCameraPos = CloudRenderer.RelativeCameraPos.BELOW_CLOUDS; } else { relativeCameraPos = CloudRenderer.RelativeCameraPos.INSIDE_CLOUDS; } double d = cameraPosition.x + ticks * 0.030000001F; double e = cameraPosition.z + 3.96F; double h = this.texture.width * 12.0; double l = this.texture.height * 12.0; d -= Mth.floor(d / h) * h; e -= Mth.floor(e / l) * l; int m = Mth.floor(d / 12.0); int n = Mth.floor(e / 12.0); float o = (float)(d - m * 12.0F); float p = (float)(e - n * 12.0F); boolean bl = cloudStatus == CloudStatus.FANCY; RenderPipeline renderPipeline = bl ? RenderPipelines.CLOUDS : RenderPipelines.FLAT_CLOUDS; if (this.needsRebuild || m != this.prevCellX || n != this.prevCellZ || relativeCameraPos != this.prevRelativeCameraPos || cloudStatus != this.prevType) { this.needsRebuild = false; this.prevCellX = m; this.prevCellZ = n; this.prevRelativeCameraPos = relativeCameraPos; this.prevType = cloudStatus; this.utb.rotate(); try (GpuBuffer.MappedView mappedView = RenderSystem.getDevice().createCommandEncoder().mapBuffer(this.utb.currentBuffer(), false, true)) { this.buildMesh(relativeCameraPos, mappedView.data(), m, n, bl, j); this.quadCount = mappedView.data().position() / 3; } } if (this.quadCount != 0) { try (GpuBuffer.MappedView mappedView = RenderSystem.getDevice().createCommandEncoder().mapBuffer(this.ubo.currentBuffer(), false, true)) { Std140Builder.intoBuffer(mappedView.data()) .putVec4(ARGB.redFloat(cloudColor), ARGB.greenFloat(cloudColor), ARGB.blueFloat(cloudColor), 1.0F) .putVec3(-o, f, -p) .putVec3(12.0F, 4.0F, 12.0F); } GpuBufferSlice gpuBufferSlice = RenderSystem.getDynamicUniforms() .writeTransform(RenderSystem.getModelViewMatrix(), new Vector4f(1.0F, 1.0F, 1.0F, 1.0F), new Vector3f(), new Matrix4f(), 0.0F); RenderTarget renderTarget = Minecraft.getInstance().getMainRenderTarget(); RenderTarget renderTarget2 = Minecraft.getInstance().levelRenderer.getCloudsTarget(); RenderSystem.AutoStorageIndexBuffer autoStorageIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.Mode.QUADS); GpuBuffer gpuBuffer = autoStorageIndexBuffer.getBuffer(6 * this.quadCount); GpuTextureView gpuTextureView; GpuTextureView gpuTextureView2; if (renderTarget2 != null) { gpuTextureView = renderTarget2.getColorTextureView(); gpuTextureView2 = renderTarget2.getDepthTextureView(); } else { gpuTextureView = renderTarget.getColorTextureView(); gpuTextureView2 = renderTarget.getDepthTextureView(); } try (RenderPass renderPass = RenderSystem.getDevice() .createCommandEncoder() .createRenderPass(() -> "Clouds", gpuTextureView, OptionalInt.empty(), gpuTextureView2, OptionalDouble.empty())) { renderPass.setPipeline(renderPipeline); RenderSystem.bindDefaultUniforms(renderPass); renderPass.setUniform("DynamicTransforms", gpuBufferSlice); renderPass.setIndexBuffer(gpuBuffer, autoStorageIndexBuffer.type()); renderPass.setVertexBuffer(0, RenderSystem.getQuadVertexBuffer()); renderPass.setUniform("CloudInfo", this.ubo.currentBuffer()); renderPass.setUniform("CloudFaces", this.utb.currentBuffer()); renderPass.setPipeline(renderPipeline); renderPass.drawIndexed(0, 0, 6 * this.quadCount, 1); } } } } private void buildMesh(CloudRenderer.RelativeCameraPos relativeCameraPos, ByteBuffer buffer, int cellX, int cellZ, boolean fancyClouds, int size) { if (this.texture != null) { long[] ls = this.texture.cells; int i = this.texture.width; int j = this.texture.height; for (int k = 0; k <= 2 * size; k++) { for (int l = -k; l <= k; l++) { int m = k - Math.abs(l); if (m >= 0 && m <= size && l * l + m * m <= size * size) { if (m != 0) { this.tryBuildCell(relativeCameraPos, buffer, cellX, cellZ, fancyClouds, l, i, -m, j, ls); } this.tryBuildCell(relativeCameraPos, buffer, cellX, cellZ, fancyClouds, l, i, m, j, ls); } } } } } private void tryBuildCell( CloudRenderer.RelativeCameraPos relativeCameraPos, ByteBuffer buffer, int cellX, int cellZ, boolean fancyClouds, int x, int width, int z, int height, long[] cells ) { int i = Math.floorMod(cellX + x, width); int j = Math.floorMod(cellZ + z, height); long l = cells[i + j * width]; if (l != 0L) { if (fancyClouds) { this.buildExtrudedCell(relativeCameraPos, buffer, x, z, l); } else { this.buildFlatCell(buffer, x, z); } } } private void buildFlatCell(ByteBuffer buffer, int cellX, int cellZ) { this.encodeFace(buffer, cellX, cellZ, Direction.DOWN, 32); } private void encodeFace(ByteBuffer buffer, int cellX, int cellZ, Direction face, int offset) { int i = face.get3DDataValue() | offset; i |= (cellX & 1) << 7; i |= (cellZ & 1) << 6; buffer.put((byte)(cellX >> 1)).put((byte)(cellZ >> 1)).put((byte)i); } private void buildExtrudedCell(CloudRenderer.RelativeCameraPos relativeCameraPos, ByteBuffer buffer, int cellX, int cellZ, long cellData) { if (relativeCameraPos != CloudRenderer.RelativeCameraPos.BELOW_CLOUDS) { this.encodeFace(buffer, cellX, cellZ, Direction.UP, 0); } if (relativeCameraPos != CloudRenderer.RelativeCameraPos.ABOVE_CLOUDS) { this.encodeFace(buffer, cellX, cellZ, Direction.DOWN, 0); } if (isNorthEmpty(cellData) && cellZ > 0) { this.encodeFace(buffer, cellX, cellZ, Direction.NORTH, 0); } if (isSouthEmpty(cellData) && cellZ < 0) { this.encodeFace(buffer, cellX, cellZ, Direction.SOUTH, 0); } if (isWestEmpty(cellData) && cellX > 0) { this.encodeFace(buffer, cellX, cellZ, Direction.WEST, 0); } if (isEastEmpty(cellData) && cellX < 0) { this.encodeFace(buffer, cellX, cellZ, Direction.EAST, 0); } boolean bl = Math.abs(cellX) <= 1 && Math.abs(cellZ) <= 1; if (bl) { for (Direction direction : Direction.values()) { this.encodeFace(buffer, cellX, cellZ, direction, 16); } } } public void markForRebuild() { this.needsRebuild = true; } public void endFrame() { this.ubo.rotate(); } public void close() { this.ubo.close(); if (this.utb != null) { this.utb.close(); } } @Environment(EnvType.CLIENT) static enum RelativeCameraPos { ABOVE_CLOUDS, INSIDE_CLOUDS, BELOW_CLOUDS; } @Environment(EnvType.CLIENT) public record TextureData(long[] cells, int width, int height) { } }