getTransparentBuffers()
+ {
+ return this.transparentBuffers;
+ }
+
+ public void clearChunk()
+ {
+ this.opaqueBuffers.setTotalElementCount(0);
+ this.transparentBuffers.ifPresent(bufferSet -> bufferSet.setTotalElementCount(0));
+ }
+
+ public void setBounds(float minX, float minY, float minZ, float maxX, float maxY, float maxZ)
+ {
+ this.boundsMinX = minX;
+ this.boundsMinY = minY;
+ this.boundsMinZ = minZ;
+ this.boundsMaxX = maxX;
+ this.boundsMaxY = maxY;
+ this.boundsMaxZ = maxZ;
+ }
+
+ public void setHeights(float minHeight, float maxHeight)
+ {
+ this.minHeight = minHeight;
+ this.maxHeight = maxHeight;
+ }
+
+ public void resetLastGenTime()
+ {
+ this.ticksSinceLastGen = 0;
+ }
+
+ public int getTicksSinceLastGen()
+ {
+ return this.ticksSinceLastGen;
+ }
+
+ public float getBoundsMinX()
+ {
+ return this.boundsMinX;
+ }
+
+ public float getBoundsMinY()
+ {
+ return this.boundsMinY;
+ }
+
+ public float getBoundsMinZ()
+ {
+ return this.boundsMinZ;
+ }
+
+ public float getBoundsMaxX()
+ {
+ return this.boundsMaxX;
+ }
+
+ public float getBoundsMaxY()
+ {
+ return this.boundsMaxY;
+ }
+
+ public float getBoundsMaxZ()
+ {
+ return this.boundsMaxZ;
+ }
+
+ public float getMinHeight()
+ {
+ return this.minHeight;
+ }
+
+ public float getMaxHeight()
+ {
+ return this.maxHeight;
+ }
+
+ public float getAlpha(float partialTick)
+ {
+ return Mth.lerp(partialTick, this.alphaO, this.alpha);
+ }
+
+ public void destroy()
+ {
+ this.opaqueBuffers.destroy();
+ this.transparentBuffers.ifPresent(MeshChunk.BufferSet::destroy);
+ }
+
+ public static class BufferSet
+ {
+ private int bufferId = -1;
+ private @Nullable ByteBuffer buffer;
+ private int elementCount;
+ private final int bufferSize;
+ private final int maxElements;
+ private final int elementOffset;
+
+ public BufferSet(int maxElements, int elementOffset, int bytesPerElement)
+ {
+ this.bufferId = GL15.glGenBuffers();
+ this.maxElements = maxElements;
+ this.elementOffset = elementOffset;
+ this.bufferSize = maxElements * bytesPerElement;
+ this.buffer = MemoryTracker.create(this.bufferSize);
+ GL15.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, this.bufferId);
+ GL15.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, this.buffer, GL15.GL_DYNAMIC_DRAW);
+ GL15.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, 0);
+ }
+
+ public void setTotalElementCount(int count)
+ {
+ this.elementCount = count;
+ }
+
+ public int getElementCount()
+ {
+ return this.elementCount;
+ }
+
+ public int getMaxElements()
+ {
+ return this.maxElements;
+ }
+
+ public int getElementOffset()
+ {
+ return this.elementOffset;
+ }
+
+ public int getBufferSize()
+ {
+ return this.bufferSize;
+ }
+
+ public int getBufferId()
+ {
+ return this.bufferId;
+ }
+
+ public void destroy()
+ {
+ this.elementCount = 0;
+
+ if (this.bufferId >= 0)
+ {
+ RenderSystem.glDeleteBuffers(this.bufferId);
+ this.bufferId = -1;
+ }
+
+ if (this.buffer != null)
+ {
+ MemoryUtil.memFree(this.buffer);
+ this.buffer = null;
+ }
+ }
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator.java b/dev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator.java
new file mode 100644
index 00000000..f2ae91fd
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator.java
@@ -0,0 +1,1149 @@
+package dev.nonamecrackers2.simpleclouds.client.mesh.generator;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Queue;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL31;
+import org.lwjgl.opengl.GL41;
+import org.lwjgl.opengl.GL42;
+import org.lwjgl.opengl.GL43;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Queues;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import dev.nonamecrackers2.simpleclouds.SimpleCloudsMod;
+import dev.nonamecrackers2.simpleclouds.client.mesh.LevelOfDetailOptions;
+import dev.nonamecrackers2.simpleclouds.client.mesh.RendererInitializeResult;
+import dev.nonamecrackers2.simpleclouds.client.mesh.chunk.MeshChunk;
+import dev.nonamecrackers2.simpleclouds.client.mesh.instancing.InstanceableMesh;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetailConfig;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.PreparedChunk;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.BindingManager;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.ShaderStorageBufferObject;
+import dev.nonamecrackers2.simpleclouds.client.shader.compute.ComputeShader;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudInfo;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.mixin.MixinFrustumAccessor;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.util.Mth;
+import net.minecraft.world.phys.AABB;
+
+/**
+ * Abstract mesh generator class that generates a cloud vertex mesh using computer shaders. Implementations are only available on the render thread.
+ *
+ * Use {@link CloudMeshGenerator#init} to initialize the mesh generator. This will initialize all needed buffers. Note
+ * that this is an expensive class and having multiple instances in one environment can cause GPU memory to run out quick (including
+ * available SSBO bindings).
+ *
+ * Use {@link CloudMeshGenerator#tick} each frame to generate the mesh at a fixed interval of frames
+ * (defined by {@link CloudMeshGenerator#setMeshGenInterval}) or use {@link CloudMeshGenerator#generateMesh} to generate
+ * it in a single call.
+ *
+ * Use {@link CloudMeshGenerator#render} to render the currently generated cloud mesh.
+ *
+ * @author nonamecrackers2
+ */
+public abstract class CloudMeshGenerator
+{
+ private static final Logger LOGGER = LogManager.getLogger("simpleclouds/CloudMeshGenerator");
+
+ public static final ResourceLocation MAIN_CUBE_MESH_GENERATOR = SimpleCloudsMod.id("cube_mesh");
+ public static final int MAX_NOISE_LAYERS = 4;
+ public static final int VERTICAL_CHUNK_SPAN = 8;
+ public static final int LOCAL_SIZE = 8;
+ public static final int WORK_SIZE = SimpleCloudsConstants.CHUNK_SIZE / LOCAL_SIZE;
+ public static final int TICKS_UNTIL_FADE_RESET = 120;
+
+ //Opaque
+ public static final int BYTES_PER_SIDE_INFO = 24;
+ public static final int MAX_SIDE_INFO_BUFFER_SIZE = 50331648;
+ public static final String SIDE_INFO_BUFFER_NAME = "SideInfoBuffer";
+ public static final String TOTAL_SIDES_NAME = "TotalSides";
+ public static final String SIDES_PER_CHUNK_NAME = "SidesPerChunk";
+ //Transparent
+ public static final int BYTES_PER_CUBE_INFO = 24;
+ public static final int MAX_TRANSPARENT_CUBE_INFO_BUFFER_SIZE = 50331648;
+ public static final String TRANSPARENT_CUBE_INFO_BUFFER_NAME = "TransparentCubeInfoBuffer";
+ public static final String TRANSPARENT_TOTAL_CUBES_NAME = "TotalTransparentCubes";
+ public static final String TRANSPARENT_CUBES_PER_CHUNK_NAME = "TransparentCubesPerChunk";
+
+ public static final String NOISE_LAYERS_NAME = "NoiseLayers";
+ public static final String LAYER_GROUPINGS_NAME = "LayerGroupings";
+
+ protected final ResourceLocation meshShaderLoc;
+ protected final int shaderType;
+ protected final boolean fadeNearOrigin;
+ protected final boolean shadedClouds;
+ protected final boolean useTransparency;
+ protected final LevelOfDetailConfig lodConfig;
+ protected final boolean useFixedMeshDataSectionSize;
+ protected @Nullable List chunks;
+ protected final List completedGenTasks = Lists.newArrayList();
+ protected final Queue chunkGenTasks = Queues.newArrayDeque();
+ protected final Supplier meshGenIntervalCalculator;
+ protected int meshGenInterval = 1;
+ protected int tasksPerTick;
+ protected @Nullable ComputeShader shader;
+
+ protected @Nullable InstanceableMesh sideMesh;
+ protected @Nullable InstanceableMesh cubeMesh;
+
+ // Left is for opaque geometry, right is for transparent
+ protected Pair meshGenStatus = Pair.of(CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED, CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED);
+ protected float scrollX;
+ protected float scrollY;
+ protected float scrollZ;
+ protected boolean testFacesFacingAway;
+ private float fadeStart;
+ private float fadeEnd;
+ private float cullDistance;
+ private int transparencyDistance;
+
+ private int opaqueBufferSize;
+ private int opaqueBufferBytesUsed;
+ private int transparentBufferSize;
+ private int transparentBufferBytesUsed;
+ private int opaqueBytesPerChunk;
+ private int transparentBytesPerChunk;
+
+ /**
+ * Creates a cloud mesh generator, but does not initialize it for generating (use {@link CloudMeshGenerator#init})
+ *
+ * @param meshShaderLoc
+ * The location of the cloud mesh generator compute shader
+ * @param lodConfig
+ * A level of detail configuration
+ * @param meshGenInterval
+ * The frame interval at which the generate the cloud mesh
+ */
+ public CloudMeshGenerator(ResourceLocation meshShaderLoc, int shaderType, boolean fadeNearOrigin, boolean shadedClouds, LevelOfDetailConfig lodConfig, Supplier meshGenIntervalCalculator, boolean useTransparency, boolean fixedMeshDataSectionSize)
+ {
+ this.meshShaderLoc = meshShaderLoc;
+ this.shaderType = shaderType;
+ this.fadeNearOrigin = fadeNearOrigin;
+ this.shadedClouds = shadedClouds;
+ this.useFixedMeshDataSectionSize = fixedMeshDataSectionSize;
+
+ this.lodConfig = lodConfig;
+ this.meshGenIntervalCalculator = meshGenIntervalCalculator;
+ this.useTransparency = useTransparency;
+
+ float maxRadius = this.getCloudAreaMaxRadius();
+ this.fadeStart = 0.9F * maxRadius;
+ this.fadeEnd = maxRadius;
+ this.transparencyDistance = (int)maxRadius / 2;
+ }
+
+ public boolean fadeNearOriginEnabled()
+ {
+ return this.fadeNearOrigin;
+ }
+
+ public boolean shadedCloudsEnabled()
+ {
+ return this.shadedClouds;
+ }
+
+ public boolean transparencyEnabled()
+ {
+ return this.useTransparency;
+ }
+
+ public boolean usesFixedMeshDataSectionSize()
+ {
+ return this.useFixedMeshDataSectionSize;
+ }
+
+ public LevelOfDetailConfig getLodConfig()
+ {
+ return this.lodConfig;
+ }
+
+ /**
+ * Specifies if faces not facing the camera should be tested during
+ * mesh generation on the GPU for whether they should be generated or not.
+ *
+ * Enabling can improve performance at the cost of some visual artifacts
+ * or an incomplete cloud mesh
+ *
+ * @param flag
+ * @return
+ */
+ public CloudMeshGenerator setTestFacesFacingAway(boolean flag)
+ {
+ this.testFacesFacingAway = flag;
+ return this;
+ }
+
+ /**
+ * Sets the fade start and end distances as decimal percentages
+ *
+ * @param fadeStart
+ * @param fadeEnd
+ */
+ public CloudMeshGenerator setFadeDistances(float fadeStart, float fadeEnd)
+ {
+ float fs = fadeStart;
+ float fe = fadeEnd;
+ if (fs > fe)
+ {
+ fs = fadeEnd;
+ fe = fadeStart;
+ }
+ this.fadeStart = fs * (float)this.getCloudAreaMaxRadius();
+ this.fadeEnd = fe * (float)this.getCloudAreaMaxRadius();
+ return this;
+ }
+
+ public CloudMeshGenerator setTransparencyRenderDistance(float percentage)
+ {
+ this.transparencyDistance = Mth.floor(percentage * (float)this.getCloudAreaMaxRadius());
+ return this;
+ }
+
+ public float getFadeStart()
+ {
+ return this.fadeStart;
+ }
+
+ public float getFadeEnd()
+ {
+ return this.fadeEnd;
+ }
+
+ public int getCloudAreaMaxRadius()
+ {
+ return this.lodConfig.getEffectiveChunkSpan() * WORK_SIZE * LOCAL_SIZE / 2;
+ }
+
+ public void setCullDistance(float dist)
+ {
+ if (dist <= 0.0F)
+ throw new IllegalArgumentException("Cull distance must be greater than zero");
+ this.cullDistance = dist;
+ }
+
+ public void disableCullDistance()
+ {
+ this.cullDistance = 0.0F;
+ }
+
+ public void setScroll(float x, float y, float z)
+ {
+ this.scrollX = x;
+ this.scrollY = y;
+ this.scrollZ = z;
+ }
+
+ public Pair getMeshGenStatus()
+ {
+ return this.meshGenStatus;
+ }
+
+ public @Nullable InstanceableMesh getSideMesh()
+ {
+ return this.sideMesh;
+ }
+
+ public @Nullable InstanceableMesh getCubeMesh()
+ {
+ return this.cubeMesh;
+ }
+
+ public int getOpaqueBufferSize()
+ {
+ return this.opaqueBufferSize;
+ }
+
+ public int getOpaqueBufferBytesUsed()
+ {
+ return this.opaqueBufferBytesUsed;
+ }
+
+ public int getTransparentBufferSize()
+ {
+ return this.transparentBufferSize;
+ }
+
+ public int getTransparentBufferBytesUsed()
+ {
+ return this.transparentBufferBytesUsed;
+ }
+
+ public int getOpaqueBytesPerChunk()
+ {
+ return this.opaqueBytesPerChunk;
+ }
+
+ public int getTransparentBytesPerChunk()
+ {
+ return this.transparentBytesPerChunk;
+ }
+
+ public int getTotalMeshChunks()
+ {
+ if (this.chunks == null)
+ return 0;
+ return this.chunks.size();
+ }
+
+ public int getMeshGenInterval()
+ {
+ return this.meshGenInterval;
+ }
+
+ public void close()
+ {
+ RenderSystem.assertOnRenderThreadOrInit();
+
+ this.opaqueBufferBytesUsed = 0;
+ this.opaqueBufferSize = 0;
+ this.opaqueBytesPerChunk = 0;
+ this.transparentBufferBytesUsed = 0;
+ this.transparentBufferSize = 0;
+ this.transparentBytesPerChunk = 0;
+
+ GL42.glMemoryBarrier(GL42.GL_ALL_BARRIER_BITS);
+ this.chunkGenTasks.clear();
+ this.completedGenTasks.clear();
+
+ if (this.shader != null)
+ this.shader.close();
+ this.shader = null;
+
+ if (this.chunks != null)
+ {
+ for (MeshChunk chunk : this.chunks)
+ chunk.destroy();
+ this.chunks = null;
+ }
+
+ if (this.sideMesh != null)
+ {
+ this.sideMesh.destroy();
+ this.sideMesh = null;
+ }
+
+ if (this.cubeMesh != null)
+ {
+ this.cubeMesh.destroy();
+ this.cubeMesh = null;
+ }
+ }
+
+ public boolean canRender()
+ {
+ return this.chunks != null;
+ }
+
+ public final RendererInitializeResult init(ResourceManager manager)
+ {
+ RendererInitializeResult.Builder builder = RendererInitializeResult.builder();
+
+ if (!RenderSystem.isOnRenderThreadOrInit())
+ return builder.errorUnknown(new IllegalStateException("Init not called on render thread"), "Mesh Generator; Head").build();
+
+ this.opaqueBufferBytesUsed = 0;
+ this.opaqueBufferSize = 0;
+ this.opaqueBytesPerChunk = 0;
+ this.transparentBufferBytesUsed = 0;
+ this.transparentBufferSize = 0;
+ this.transparentBytesPerChunk = 0;
+
+ GL42.glMemoryBarrier(GL42.GL_ALL_BARRIER_BITS);
+ this.chunkGenTasks.clear();
+ this.completedGenTasks.clear();
+
+ LOGGER.debug("Beginning mesh generator initialization");
+
+ if (this.shader != null)
+ {
+ LOGGER.debug("Freeing mesh compute shader");
+ this.shader.close();
+ this.shader = null;
+ }
+
+ if (this.chunks != null)
+ {
+ for (MeshChunk chunk : this.chunks)
+ chunk.destroy();
+ this.chunks = null;
+ }
+
+ try
+ {
+ LOGGER.debug("Creating mesh compute shader...");
+ this.shader = this.createShader(manager);
+ this.setupShader();
+ }
+ catch (IOException e)
+ {
+ //LOGGER.warn("Failed to load compute shader", e);
+ builder.errorCouldNotLoadMeshScript(e, "Mesh Generator; Compute Shader");
+ }
+ catch (Exception e)
+ {
+ builder.errorRecommendations(e, "Mesh Generator; Compute Shader");
+ }
+
+ try
+ {
+ this.initExtra(manager);
+ }
+ catch (Exception e)
+ {
+ builder.errorUnknown(e, "Init Extra");
+ }
+
+ List preparedChunks = this.getLodConfig().getPreparedChunks();
+ ImmutableList.Builder meshChunks = ImmutableList.builder();
+ int totalPreparedChunks = preparedChunks.size();
+ this.opaqueBytesPerChunk = Mth.ceil(this.opaqueBufferSize / totalPreparedChunks);
+ this.transparentBytesPerChunk = Mth.ceil(this.transparentBufferSize / totalPreparedChunks);
+ if (!this.useFixedMeshDataSectionSize)
+ {
+ this.opaqueBytesPerChunk *= 4;
+ this.transparentBytesPerChunk *= 4;
+ }
+ int maxOpaqueElements = Mth.floor((float)this.opaqueBytesPerChunk / (float)BYTES_PER_SIDE_INFO);
+ int maxTransparentElements = Mth.floor((float)this.transparentBytesPerChunk / (float)BYTES_PER_CUBE_INFO);
+ int opaqueElementOffset = 0;
+ int transparentElementOffset = 0;
+ for (PreparedChunk chunk : preparedChunks)
+ {
+ meshChunks.add(new MeshChunk(chunk, maxOpaqueElements, opaqueElementOffset, BYTES_PER_SIDE_INFO, maxTransparentElements, transparentElementOffset, BYTES_PER_CUBE_INFO, this.useTransparency));
+ opaqueElementOffset += maxOpaqueElements;
+ transparentElementOffset += maxTransparentElements;
+ }
+ this.chunks = meshChunks.build();
+
+ LOGGER.debug("Opaque buffer size: {} bytes, transparent buffer size: {} bytes", this.opaqueBufferSize, this.transparentBufferSize);
+
+ if (this.sideMesh != null)
+ this.sideMesh.destroy();
+ this.sideMesh = InstanceableMesh.defaultSide();
+
+ if (this.cubeMesh != null)
+ this.cubeMesh.destroy();
+ this.cubeMesh = InstanceableMesh.defaultCube();
+
+ BindingManager.printDebug();
+
+ LOGGER.debug("Finished initializing mesh generator");
+
+ return builder.build();
+ }
+
+ protected ComputeShader createShader(ResourceManager manager) throws IOException
+ {
+ ImmutableMap parameters = ImmutableMap.of(
+ "TYPE", String.valueOf(this.shaderType),
+ "FADE_NEAR_ORIGIN", this.fadeNearOrigin ? "1" : "0",
+ "STYLE", this.shadedClouds ? "1" : "0",
+ "TRANSPARENCY", this.useTransparency ? "1" : "0",
+ "FIXED_SECTION_SIZE", this.useFixedMeshDataSectionSize ? "1" : "0"
+ );
+ return ComputeShader.loadShader(this.meshShaderLoc, manager, LOCAL_SIZE, LOCAL_SIZE, LOCAL_SIZE, parameters);
+ }
+
+ protected void setupShader()
+ {
+ this.opaqueBufferSize = this.createBuffers(
+ TOTAL_SIDES_NAME,
+ SIDES_PER_CHUNK_NAME,
+ SIDE_INFO_BUFFER_NAME,
+ MAX_SIDE_INFO_BUFFER_SIZE * (this.useFixedMeshDataSectionSize ? 4 : 1)
+ );
+
+ if (this.useTransparency)
+ {
+ this.transparentBufferSize = this.createBuffers(
+ TRANSPARENT_TOTAL_CUBES_NAME,
+ TRANSPARENT_CUBES_PER_CHUNK_NAME,
+ TRANSPARENT_CUBE_INFO_BUFFER_NAME,
+ MAX_TRANSPARENT_CUBE_INFO_BUFFER_SIZE * (this.useFixedMeshDataSectionSize ? 4 : 1)
+ );
+ }
+
+ this.shader.forUniform("TotalLodLevels", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.lodConfig.getLods().length);
+ });
+
+ this.uploadFadeData();
+ }
+
+ private void uploadFadeData()
+ {
+ if (this.shader == null || !this.shader.isValid())
+ return;
+
+ this.shader.forUniform("TransparencyDistance", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.transparencyDistance);
+ });
+ this.shader.forUniform("FadeStart", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, this.fadeStart);
+ });
+ this.shader.forUniform("FadeEnd", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, this.fadeEnd);
+ });
+ }
+
+ private int createBuffers(String totalCounterName, String countPerChunkName, String elementInfoBufferName, int maxSize)
+ {
+ if (!this.useFixedMeshDataSectionSize)
+ {
+ ShaderStorageBufferObject totalCountBuffer = this.shader.createAndBindSSBO(totalCounterName, GL15.GL_DYNAMIC_COPY);
+ totalCountBuffer.allocateBuffer(4);
+ totalCountBuffer.writeData(b -> {
+ b.putInt(0, 0);
+ }, 4, false);
+ }
+
+ int bufferSize = this.shader.createAndBindSSBO(elementInfoBufferName, GL15.GL_DYNAMIC_COPY).allocateBuffer(maxSize);
+
+ int totalChunks = this.getLodConfig().getPreparedChunks().size();
+ int countPerChunkBufferSize = totalChunks * 4;
+ ShaderStorageBufferObject countPerChunkBuffer = this.shader.createAndBindSSBO(countPerChunkName, GL15.GL_DYNAMIC_COPY);
+ countPerChunkBuffer.allocateBuffer(countPerChunkBufferSize);
+ countPerChunkBuffer.writeData(b ->
+ {
+ for (int i = 0; i < totalChunks; i++)
+ b.putInt(0);
+ b.rewind();
+ }, countPerChunkBufferSize, false);
+
+ return bufferSize;
+ }
+
+ protected void initExtra(ResourceManager manager) throws IOException {}
+
+ /**
+ * Generates the entire cloud mesh at the origin at once
+ */
+ public void generateMesh()
+ {
+ RenderSystem.assertOnRenderThread();
+
+ if (this.shader == null || !this.shader.isValid())
+ return;
+
+ this.prepareMeshGen(0.0D, 0.0D, 0.0D, 0.0F, 0.0F, null, 1, 1.0F);
+
+ if (!this.chunkGenTasks.isEmpty())
+ this.doMeshGenning(this.chunkGenTasks.size());
+
+ this.meshGenStatus = this.finalizeMeshGen();
+ this.completedGenTasks.clear();
+ }
+
+ public void worldTick()
+ {
+ if (this.chunks != null)
+ this.chunks.forEach(MeshChunk::tick);
+ }
+
+ /**
+ * Generates the cloud mesh on a per-frame basis
+ *
+ * @param originX
+ * @param originY
+ * @param originZ
+ * @param frustum
+ */
+ public void genTick(double originX, double originY, double originZ, @Nullable Frustum frustum, float partialTick)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ if (this.shader == null || !this.shader.isValid())
+ return;
+
+ float chunkSize = (float)SimpleCloudsConstants.CHUNK_SIZE;
+ float meshGenOffsetX = (float)Mth.floor(originX / chunkSize) * chunkSize;
+ float meshGenOffsetZ = (float)Mth.floor(originZ / chunkSize) * chunkSize;
+
+ if (this.chunkGenTasks.isEmpty()) //If we have no chunk gen tasks
+ {
+ this.meshGenStatus = this.finalizeMeshGen(); //Split the combined mesh data from the GPU, and store them in the VBOs for each chunk that was generated
+ this.completedGenTasks.clear(); //Clear the chunk gen tasks
+
+ //Prepare the next batch of chunks to generate meshes for
+ this.meshGenInterval = this.meshGenIntervalCalculator.get();
+ if (this.meshGenInterval <= 0)
+ throw new RuntimeException("Mesh gen interval is <= 0");
+ this.tasksPerTick = this.prepareMeshGen(originX, originY, originZ, meshGenOffsetX, meshGenOffsetZ, frustum, this.meshGenInterval, partialTick);
+ }
+ else
+ {
+ this.onOffGen();
+ }
+
+ //If there are mesh gen tasks, we do mesh genning
+ if (!this.chunkGenTasks.isEmpty())
+ this.doMeshGenning(this.tasksPerTick);
+ }
+
+ private static CloudMeshGenerator.MeshGenStatus fixedIterateAndCopyToChunkBuffer(int copyBufferId, int copyBufferSizeBytes, Collection chunks, Function byteOffsetPerChunk, Function chunkBufferId, Function bytesToCopyPerChunk, Function bufferSizeBytesPerChunk)
+ {
+ CloudMeshGenerator.MeshGenStatus result = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_READ_BUFFER, copyBufferId);
+
+ for (MeshChunk chunk : chunks)
+ {
+ int bytesToCopy = bytesToCopyPerChunk.apply(chunk);
+ if (bytesToCopy > 0)
+ {
+ int maxSize = bufferSizeBytesPerChunk.apply(chunk);
+ if (bytesToCopy > maxSize) // Too many bytes to go in to the chunk mesh buffer
+ {
+ bytesToCopy = maxSize;
+ result = CloudMeshGenerator.MeshGenStatus.CHUNK_OVERFLOW;
+ }
+
+ int byteOffset = byteOffsetPerChunk.apply(chunk);
+ if (byteOffset + bytesToCopy > copyBufferSizeBytes) // TODO: Account for this overflow using mesh gen status
+ {
+ //TODO: Make sure this uses multiples of the size of a single element to avoid cutting off a single element
+ bytesToCopy = copyBufferSizeBytes - byteOffset;
+ if (bytesToCopy <= 0)
+ continue;
+ }
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, chunkBufferId.apply(chunk));
+ GL31.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, byteOffset, 0, bytesToCopy);
+ }
+ }
+
+ return result;
+ }
+
+ private static CloudMeshGenerator.MeshGenStatus packedIterateAndCopyToChunkBuffer(int copyBufferId, int copyBufferSizeBytes, Collection chunks, Function chunkBufferId, Function bytesToCopyPerChunk, Function bufferSizeBytesPerChunk)
+ {
+ CloudMeshGenerator.MeshGenStatus result = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_READ_BUFFER, copyBufferId);
+
+ int currentBytes = 0;
+ for (MeshChunk chunk : chunks)
+ {
+ int totalBytes = bytesToCopyPerChunk.apply(chunk);
+ if (totalBytes > 0) //If the chunk has data that needs copying over
+ {
+ int lastBytesOffset = totalBytes;
+ int maxSize = bufferSizeBytesPerChunk.apply(chunk);
+ if (lastBytesOffset > maxSize) //Make sure we don't go over the maximum the chunk buffer can hold
+ {
+ lastBytesOffset = maxSize;
+ result = CloudMeshGenerator.MeshGenStatus.CHUNK_OVERFLOW;
+ }
+ boolean stop = false;
+ if (currentBytes + lastBytesOffset > copyBufferSizeBytes) //If the the byte offset will go over the size of the copy buffer, clamp
+ {
+ lastBytesOffset = copyBufferSizeBytes - currentBytes;
+ if (lastBytesOffset <= 0) // If it becomes negative however, we don't want to attempt to copy data over
+ return CloudMeshGenerator.MeshGenStatus.MESH_POOL_OVERFLOW;
+ stop = true; // After copying this data over we will stop, since there is no more space in the copy buffer to read data from
+ }
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, chunkBufferId.apply(chunk));
+ GL31.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, currentBytes, 0, lastBytesOffset);
+
+ currentBytes += totalBytes;
+
+ if (stop)
+ return CloudMeshGenerator.MeshGenStatus.MESH_POOL_OVERFLOW;
+ }
+ }
+
+ return result;
+ }
+
+ protected Pair finalizeMeshGen()
+ {
+ if (this.shader == null || !this.shader.isValid() || this.chunks == null)
+ return Pair.of(CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED, CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED);
+
+ if (this.completedGenTasks.isEmpty())
+ return Pair.of(CloudMeshGenerator.MeshGenStatus.NO_TASKS, CloudMeshGenerator.MeshGenStatus.NO_TASKS);
+
+ RenderSystem.assertOnRenderThread();
+
+ GL42.glMemoryBarrier(GL43.GL_SHADER_STORAGE_BARRIER_BIT);
+
+ CloudMeshGenerator.MeshGenStatus opaqueResult = CloudMeshGenerator.MeshGenStatus.NORMAL;
+ CloudMeshGenerator.MeshGenStatus transparentResult = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ opaqueResult = this.copyMeshData(
+ TOTAL_SIDES_NAME,
+ SIDES_PER_CHUNK_NAME,
+ SIDE_INFO_BUFFER_NAME,
+ MeshChunk::getOpaqueBuffers,
+ BYTES_PER_SIDE_INFO,
+ this.opaqueBufferSize
+ );
+
+ if (this.useTransparency)
+ {
+ transparentResult = this.copyMeshData(
+ TRANSPARENT_TOTAL_CUBES_NAME,
+ TRANSPARENT_CUBES_PER_CHUNK_NAME,
+ TRANSPARENT_CUBE_INFO_BUFFER_NAME,
+ c -> c.getTransparentBuffers().get(),
+ BYTES_PER_CUBE_INFO,
+ this.transparentBufferSize
+ );
+ }
+
+ this.opaqueBufferBytesUsed = 0;
+ this.transparentBufferBytesUsed = 0;
+ for (MeshChunk chunk : this.chunks)
+ {
+ this.opaqueBufferBytesUsed += chunk.getOpaqueBuffers().getElementCount() * BYTES_PER_SIDE_INFO;
+ chunk.getTransparentBuffers().ifPresent(bufferSet -> {
+ this.transparentBufferBytesUsed += bufferSet.getElementCount() * BYTES_PER_CUBE_INFO;
+ });
+ }
+
+ return Pair.of(opaqueResult, transparentResult);
+ }
+
+ private CloudMeshGenerator.MeshGenStatus copyMeshData(String totalCountBufferName, String countPerChunkBufferName, String elementBufferName, Function bufferSetFunction, int bytesPerElement, int elementBufferSize)
+ {
+ CloudMeshGenerator.MeshGenStatus status = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ if (!this.useFixedMeshDataSectionSize)
+ {
+ //Get the total amount of sides and indices across all chunks and reset
+ this.shader.getShaderStorageBuffer(totalCountBufferName).writeData(b -> {
+ b.putInt(0, 0);
+ }, 4, true);
+ }
+
+ //Get the amount of total sides each chunk has and reset each counter
+ this.shader.getShaderStorageBuffer(countPerChunkBufferName).readWriteData(buffer ->
+ {
+ for (CloudMeshGenerator.ChunkGenTask gennedChunk : this.completedGenTasks)
+ {
+ MeshChunk.BufferSet bufferSet = bufferSetFunction.apply(gennedChunk.chunk());
+ int index = gennedChunk.index() * 4;
+ int count = buffer.getInt(index);
+ bufferSet.setTotalElementCount(count);
+ buffer.putInt(index, 0);
+ }
+ }, this.chunks.size() * 4);
+
+ List completedChunks = this.completedGenTasks.stream().map(CloudMeshGenerator.ChunkGenTask::chunk).toList();
+
+ int elementBufferId = this.shader.getShaderStorageBuffer(elementBufferName).getId();
+ if (this.useFixedMeshDataSectionSize)
+ status = fixedIterateAndCopyToChunkBuffer(elementBufferId, elementBufferSize, completedChunks, bufferSetFunction.andThen(b -> b.getElementOffset() * bytesPerElement), bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferId), bufferSetFunction.andThen(c -> c.getElementCount() * bytesPerElement), bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferSize));
+ else
+ status = packedIterateAndCopyToChunkBuffer(elementBufferId, elementBufferSize, completedChunks, bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferId), bufferSetFunction.andThen(c -> c.getElementCount() * bytesPerElement), bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferSize));
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_READ_BUFFER, 0);
+ GlStateManager._glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, 0);
+
+ return status;
+ }
+
+ /**
+ * Queues a list of chunk gen tasks for each chunk in this mesh generator
+ *
+ * @param meshGenOffsetX
+ * @param meshGenOffsetZ
+ * @param frustum
+ * Culling frustum, null for no culling
+ * @param genInterval
+ * How many frames mesh genning should take
+ * @return
+ */
+ protected int prepareMeshGen(double originX, double originY, double originZ, float meshGenOffsetX, float meshGenOffsetZ, @Nullable Frustum frustum, int genInterval, float partialTick)
+ {
+ this.shader.forUniform("Scroll", (id, loc) -> {
+ GL41.glProgramUniform3f(id, loc, this.scrollX, this.scrollY, this.scrollZ);
+ });
+ this.shader.forUniform("Wiggle", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, (this.scrollX + this.scrollY + this.scrollZ) / 5.0F);
+ });
+ this.shader.forUniform("Origin", (id, loc) -> {
+ GL41.glProgramUniform3f(id, loc, (float)originX, (float)originY, (float)originZ);
+ });
+ this.shader.forUniform("TestFacesFacingAway", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.testFacesFacingAway ? 1 : 0);
+ });
+ this.uploadFadeData();
+
+ int chunkCount = 0;
+ for (int i = 0; i < this.chunks.size(); i++)
+ {
+ if (this.queueChunkMeshGenTaskOrClear(this.chunks.get(i), i, meshGenOffsetX, meshGenOffsetZ, frustum))
+ chunkCount++;
+ }
+ return Mth.ceil((float)chunkCount / (float)genInterval);
+ }
+
+ protected void onOffGen()
+ {
+ //We read these SSBOs here to avoid weird frame spikes when in fullscreen V-Sync, not sure why it happens
+ if (!this.useFixedMeshDataSectionSize)
+ this.shader.getShaderStorageBuffer(TOTAL_SIDES_NAME).readWriteData(b -> {}, 4);
+ this.shader.getShaderStorageBuffer(SIDES_PER_CHUNK_NAME).readWriteData(buffer -> {}, this.chunks.size() * 4);
+ if (this.useTransparency)
+ {
+ if (!this.useFixedMeshDataSectionSize)
+ this.shader.getShaderStorageBuffer(TRANSPARENT_TOTAL_CUBES_NAME).readWriteData(b -> {}, 4);
+ this.shader.getShaderStorageBuffer(TRANSPARENT_CUBES_PER_CHUNK_NAME).readWriteData(buffer -> {}, this.chunks.size() * 4);
+ }
+ }
+
+ /**
+ * Queues a given chunk for mesh genning or clears it if empty
+ *
+ * @param chunk
+ * The given {@link MeshChunk} to generate a mesh for
+ * @param chunkIndex
+ * The index of the mesh chunk in {@code this.chunks}
+ * @param meshGenOffsetX
+ * @param meshGenOffsetZ
+ * @param frustum
+ * For frustum culling, null for no culling
+ * @return
+ */
+ protected boolean queueChunkMeshGenTaskOrClear(MeshChunk chunk, int chunkIndex, float meshGenOffsetX, float meshGenOffsetZ, @Nullable Frustum frustum)
+ {
+ PreparedChunk chunkInfo = chunk.getChunkInfo();
+ AABB bounds = chunkInfo.bounds();
+ float minX = (float)bounds.minX + meshGenOffsetX;
+ float minZ = (float)bounds.minZ + meshGenOffsetZ;
+ float maxX = (float)bounds.maxX + meshGenOffsetX;
+ float maxZ = (float)bounds.maxZ + meshGenOffsetZ;
+
+ if (frustum == null || ((MixinFrustumAccessor)frustum).simpleclouds$cubeInFrustum(minX, bounds.minY, minZ, maxX, bounds.maxY, maxZ))
+ {
+ double nearestCornerX = Math.max(Math.max(bounds.minX, -bounds.maxX), 0.0D);
+ double nearestCornerZ = Math.max(Math.max(bounds.minZ, -bounds.maxZ), 0.0D);
+ double dist = Math.sqrt(nearestCornerX * nearestCornerX + nearestCornerZ * nearestCornerZ);
+
+ if (this.cullDistance <= 0.0F || dist < this.cullDistance)
+ {
+ CloudMeshGenerator.ChunkGenSettings settings = this.determineChunkGenSettings(minX, minZ, maxX, maxZ);
+ if (settings.skipChunk())
+ {
+ chunk.clearChunk();
+ return false;
+ }
+ this.chunkGenTasks.add(new CloudMeshGenerator.ChunkGenTask(chunk, minX, (float)bounds.minY, minZ, maxX, (float)bounds.maxY, maxZ, chunkIndex, minX, 0.0F, minZ, settings.minimumHeight(), settings.maximumHeight()));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected abstract CloudMeshGenerator.ChunkGenSettings determineChunkGenSettings(float minX, float minZ, float maxX, float maxZ);
+
+ /**
+ * Does mesh generating for a given amount of chunks defined by tasksPerTick
+ *
+ * @param tasksPerTick
+ */
+ protected void doMeshGenning(int tasksPerTick)
+ {
+ for (int i = 0; i < tasksPerTick; i++)
+ {
+ CloudMeshGenerator.ChunkGenTask task = this.chunkGenTasks.poll();
+ if (task != null)
+ {
+ this.generateChunk(task);
+ this.updateMeshChunkAfterGeneration(task.chunk(), task);
+ this.completedGenTasks.add(task);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ protected void updateMeshChunkAfterGeneration(MeshChunk chunk, CloudMeshGenerator.ChunkGenTask task)
+ {
+ chunk.setBounds(task.minX(), task.minY(), task.minZ(), task.maxX(), task.maxY(), task.maxZ());
+ chunk.setHeights(task.startY(), task.endY());
+ chunk.resetLastGenTime();
+ }
+
+ /**
+ * Generates a given chunk, or completes a chunk gen task
+ *
+ * @param task
+ * @param scale
+ * @param globalOffsetX
+ * @param globalOffsetZ
+ */
+ protected void generateChunk(CloudMeshGenerator.ChunkGenTask task)
+ {
+ PreparedChunk chunkInfo = task.chunk().getChunkInfo();
+
+ int lodScale = chunkInfo.lodScale();
+ int lowestY = task.startY();
+ int height = Mth.ceil((float)(task.endY() - lowestY) / (float)lodScale);
+ int localHeightInvocations = Mth.ceil((float)height / (float)LOCAL_SIZE);
+
+ if (localHeightInvocations > 0)
+ {
+ this.shader.forUniform("ChunkIndex", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, task.index());
+ });
+ this.shader.forUniform("LodLevel", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, chunkInfo.lodLevel());
+ });
+ this.shader.forUniform("RenderOffset", (id, loc) -> {
+ GL41.glProgramUniform3f(id, loc, task.x(), task.y() + lowestY, task.z());
+ });
+ this.shader.forUniform("Scale", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, lodScale);
+ });
+ this.shader.forUniform("DoNotOccludeSide", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, chunkInfo.noOcclusionDirectionIndex());
+ });
+ if (this.useFixedMeshDataSectionSize)
+ {
+ this.shader.forUniform("OpaqueMeshDataOffset", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, task.chunk().getOpaqueBuffers().getElementOffset());
+ });
+
+ task.chunk().getTransparentBuffers().ifPresent(bufferSet ->
+ {
+ this.shader.forUniform("TransparentMeshDataOffset", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, bufferSet.getElementOffset());
+ });
+ });
+ }
+
+ this.shader.dispatch(WORK_SIZE, localHeightInvocations, WORK_SIZE, false);
+ if (!this.useFixedMeshDataSectionSize)
+ GL42.glMemoryBarrier(GL43.GL_SHADER_STORAGE_BARRIER_BIT);
+ }
+ }
+
+ public void forRenderableMeshChunks(@Nullable Frustum frustum, Function bufferSetFunction, BiConsumer function)
+ {
+ this.forRenderableMeshChunks(frustum, bufferSetFunction, function, false);
+ }
+
+ public void forRenderableMeshChunks(@Nullable Frustum frustum, Function bufferSetFunction, BiConsumer function, boolean updateFade)
+ {
+ for (MeshChunk chunk : this.chunks)
+ {
+ MeshChunk.BufferSet bufferSet = bufferSetFunction.apply(chunk);
+ if (bufferSet.getElementCount() > 0)
+ {
+ if (updateFade && chunk.getTicksSinceLastGen() > TICKS_UNTIL_FADE_RESET)
+ {
+ chunk.resetAlpha();
+ chunk.setFadeEnabled(false);
+ }
+
+ boolean render = true;
+ if (frustum != null)
+ render = ((MixinFrustumAccessor)frustum).simpleclouds$cubeInFrustum(chunk.getBoundsMinX(), chunk.getBoundsMinY(), chunk.getBoundsMinZ(), chunk.getBoundsMaxX(), chunk.getBoundsMaxY(), chunk.getBoundsMaxZ());
+
+ if (render)
+ {
+ PreparedChunk chunkInfo = chunk.getChunkInfo();
+ AABB bounds = chunkInfo.bounds();
+ double nearestCornerX = Math.max(Math.max(bounds.minX, -bounds.maxX), 0.0D);
+ double nearestCornerZ = Math.max(Math.max(bounds.minZ, -bounds.maxZ), 0.0D);
+ double dist = Math.sqrt(nearestCornerX * nearestCornerX + nearestCornerZ * nearestCornerZ);
+ if (this.cullDistance <= 0.0F || this.cullDistance > dist)
+ {
+ if (updateFade)
+ chunk.setFadeEnabled(true);
+ function.accept(chunk, bufferSet);
+ }
+ }
+ }
+ }
+ }
+
+ public void fillReport(CrashReportCategory category)
+ {
+ category.setDetail("Shader Type", this.shaderType);
+ category.setDetail("Shaded Clouds", this.shadedClouds);
+ category.setDetail("Transparency Enabled", this.useTransparency);
+ category.setDetail("Fade Near Origin", this.fadeNearOrigin);
+ category.setDetail("Compute Shader", this.shader);
+ category.setDetail("Level Of Details", 1 + this.lodConfig.getLods().length);
+ category.setDetail("Generation Frame Interval", this.meshGenInterval);
+ category.setDetail("Total Prepared Chunks", this.lodConfig.getPreparedChunks().size());
+ category.setDetail("Tasks Per Frame", this.tasksPerTick);
+ category.setDetail("Scroll", String.format("X: %s, Y: %s, Z: %s", this.scrollX, this.scrollY, this.scrollZ));
+ category.setDetail("Total Mesh Chunks", this.chunks != null ? this.chunks.size() : "null");
+ category.setDetail("Mesh Gen Status", this.meshGenStatus);
+ category.setDetail("Test Occluded Faces", this.testFacesFacingAway);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[shader_name=%s]", this.getClass().getSimpleName(), this.meshShaderLoc);
+ }
+
+ protected static CloudMeshGenerator.ChunkGenSettings skip()
+ {
+ return new CloudMeshGenerator.ChunkGenSettings(true, 0, 0);
+ }
+
+ protected static CloudMeshGenerator.ChunkGenSettings heights(int min, int max)
+ {
+ return new CloudMeshGenerator.ChunkGenSettings(false, min, max);
+ }
+
+ public static CloudMeshGenerator.Builder builder()
+ {
+ return new CloudMeshGenerator.Builder();
+ }
+
+ protected static record ChunkGenSettings(boolean skipChunk, int minimumHeight, int maximumHeight) {}
+
+ protected static record ChunkGenTask(MeshChunk chunk, float minX, float minY, float minZ, float maxX, float maxY, float maxZ, int index, float x, float y, float z, int startY, int endY) {}
+
+ public static enum MeshGenStatus
+ {
+ NOT_INITIALIZED("Not initialized", true),
+ NO_TASKS("No tasks", false),
+ NORMAL("Normal", false),
+ MESH_POOL_OVERFLOW("Mesh pool overflow", true),
+ CHUNK_OVERFLOW("Chunk overflow", true);
+
+ private String name;
+ private boolean isErroneous;
+
+ private MeshGenStatus(String name, boolean isErroneous)
+ {
+ this.name = name;
+ this.isErroneous = isErroneous;
+ }
+
+ public String getName()
+ {
+ return this.name;
+ }
+
+ public boolean isErroneous()
+ {
+ return this.isErroneous;
+ }
+ }
+
+ public static class Builder
+ {
+ private boolean fadeNearOrigin;
+ private boolean shadedClouds = true;
+ private LevelOfDetailConfig lodConfig = LevelOfDetailOptions.HIGH.getConfig();
+ private Supplier meshGenIntervalCalculator = () -> 5;
+ private boolean useTransparency = true;
+ private boolean fixedMeshDataSectionSize;
+ private float fadeStart = 0.5F;
+ private float fadeEnd = 1.0F;
+ private boolean testFacesFacingAway = false;
+
+ private Builder() {}
+
+ public Builder fadeNearOrigin(boolean flag)
+ {
+ this.fadeNearOrigin = flag;
+ return this;
+ }
+
+ public Builder shadedClouds(boolean flag)
+ {
+ this.shadedClouds = flag;
+ return this;
+ }
+
+ public Builder meshGenInterval(int interval)
+ {
+ if (interval <= 0)
+ throw new IllegalArgumentException("Mesh gen interval must be greater than 0");
+ this.meshGenIntervalCalculator = () -> interval;
+ return this;
+ }
+
+ public Builder meshGenInterval(Supplier calculator)
+ {
+ this.meshGenIntervalCalculator = calculator;
+ return this;
+ }
+
+ public Builder lodConfig(LevelOfDetailConfig config)
+ {
+ this.lodConfig = config;
+ return this;
+ }
+
+ public Builder useTransparency(boolean flag)
+ {
+ this.useTransparency = flag;
+ return this;
+ }
+
+ public Builder fixedMeshDataSectionSize(boolean flag)
+ {
+ this.fixedMeshDataSectionSize = flag;
+ return this;
+ }
+
+ public Builder fadeStart(float fadeStart)
+ {
+ this.fadeStart = fadeStart;
+ return this;
+ }
+
+ public Builder fadeEnd(float fadeEnd)
+ {
+ this.fadeEnd = fadeEnd;
+ return this;
+ }
+
+ public Builder testFacesFacingAway(boolean flag)
+ {
+ this.testFacesFacingAway = flag;
+ return this;
+ }
+
+ private T applyExtraSettings(T generator)
+ {
+ generator.setFadeDistances(this.fadeStart, this.fadeEnd);
+ generator.setTestFacesFacingAway(this.testFacesFacingAway);
+ return generator;
+ }
+
+ public MultiRegionCloudMeshGenerator createMultiRegion()
+ {
+ return this.applyExtraSettings(new MultiRegionCloudMeshGenerator(this.fadeNearOrigin, this.shadedClouds, this.lodConfig, this.meshGenIntervalCalculator, this.useTransparency, this.fixedMeshDataSectionSize));
+ }
+
+ public SingleRegionCloudMeshGenerator createSingleRegion(CloudInfo type)
+ {
+ return this.applyExtraSettings(new SingleRegionCloudMeshGenerator(this.shadedClouds, this.lodConfig, this.meshGenIntervalCalculator, this.useTransparency, this.fixedMeshDataSectionSize, type));
+ }
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/client/mesh/generator/MultiRegionCloudMeshGenerator.java b/dev/nonamecrackers2/simpleclouds/client/mesh/generator/MultiRegionCloudMeshGenerator.java
new file mode 100644
index 00000000..8226742f
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/client/mesh/generator/MultiRegionCloudMeshGenerator.java
@@ -0,0 +1,404 @@
+package dev.nonamecrackers2.simpleclouds.client.mesh.generator;
+
+import java.io.IOException;
+import java.nio.IntBuffer;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.joml.Matrix2f;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL12;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL41;
+import org.lwjgl.opengl.GL42;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.mojang.blaze3d.platform.TextureUtil;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import dev.nonamecrackers2.simpleclouds.SimpleCloudsMod;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetail;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetailConfig;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.PreparedChunk;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.BindingManager;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.ShaderStorageBufferObject;
+import dev.nonamecrackers2.simpleclouds.client.shader.compute.ComputeShader;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudInfo;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudGetter;
+import dev.nonamecrackers2.simpleclouds.common.noise.AbstractNoiseSettings;
+import dev.nonamecrackers2.simpleclouds.common.noise.NoiseSettings;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+
+public final class MultiRegionCloudMeshGenerator extends CloudMeshGenerator
+{
+ private static final Logger LOGGER = LogManager.getLogger("simpleclouds/MultiRegionCloudMeshGenerator");
+
+ private static final ResourceLocation REGION_GENERATOR_LOC = SimpleCloudsMod.id("cloud_regions");
+ private static final String LOD_SCALES_NAME = "LodScales";
+ private static final String CLOUD_REGIONS_NAME = "CloudRegions";
+ public static final int MAX_CLOUD_TYPES = 64;
+ public static final int MAX_CLOUD_FORMATIONS = 10;
+ private static final int BYTES_PER_REGION = 32;
+ private int requiredRegionTexSize;
+ private CloudGetter cloudGetter = CloudGetter.EMPTY;
+ private CloudInfo[] cachedTypes = new CloudInfo[0];
+ private @Nullable ComputeShader regionTextureGenerator;
+ private int cloudRegionTextureId = -1;
+ private int cloudRegionImageBinding = -1;
+ private boolean updateCloudTypes;
+ private int currentCloudFormationCount;
+
+ protected MultiRegionCloudMeshGenerator(boolean fadeNearOrigin, boolean shadedClouds, LevelOfDetailConfig lodConfig, Supplier meshGenIntervalCalculator, boolean useTransparency, boolean fixedMeshDataSectionSize)
+ {
+ super(CloudMeshGenerator.MAIN_CUBE_MESH_GENERATOR, 0, fadeNearOrigin, shadedClouds, lodConfig, meshGenIntervalCalculator, useTransparency, fixedMeshDataSectionSize);
+ }
+
+ public void setCloudGetter(CloudGetter getter)
+ {
+ this.cloudGetter = Objects.requireNonNull(getter, "Cloud getter cannot be null");
+ this.updateCloudTypes();
+ }
+
+ public int getCloudRegionTextureId()
+ {
+ return this.cloudRegionTextureId;
+ }
+
+ public void updateCloudTypes()
+ {
+ this.updateCloudTypes = true;
+ }
+
+ public int getTotalCloudTypes()
+ {
+ return this.cachedTypes.length;
+ }
+
+ public int getCloudFormationCount()
+ {
+ return this.currentCloudFormationCount;
+ }
+
+ @Override
+ protected void setupShader()
+ {
+ super.setupShader();
+
+ this.cachedTypes = new CloudInfo[0];
+ this.updateCloudTypes = false;
+
+ this.shader.createAndBindSSBO(NOISE_LAYERS_NAME, GL15.GL_STATIC_DRAW).allocateBuffer(AbstractNoiseSettings.Param.values().length * 4 * MAX_NOISE_LAYERS * MAX_CLOUD_TYPES);
+ this.shader.createAndBindSSBO(LAYER_GROUPINGS_NAME, GL15.GL_STATIC_DRAW).allocateBuffer(CloudInfo.BYTES_PER_TYPE * MAX_CLOUD_TYPES);
+
+ this.uploadCloudTypeData();
+ }
+
+ @Override
+ protected void initExtra(ResourceManager manager) throws IOException
+ {
+ // Cloud region texture generator compute shader
+ // This texture is a 2D array texture, with a texture for each level of detail.
+ // The red channel contains the index for a cloud type in the main mesh compute shader, and
+ // the green channel contains an "edge fade" value for smooth cloud region boundaries.
+ // When generating the cloud mesh, the main mesh compute shader samples this array texture
+ // depending on what LOD it is generating for to determine what cloud type to construct
+
+ // Create the compute shader
+
+ this.currentCloudFormationCount = 0;
+ this.requiredRegionTexSize = 0;
+
+ if (this.regionTextureGenerator != null)
+ this.regionTextureGenerator.close();
+
+ var params = ImmutableMap.of("EDGE_FADE_FACTOR", String.valueOf(SimpleCloudsConstants.REGION_EDGE_FADE_FACTOR));
+ this.regionTextureGenerator = ComputeShader.loadShader(REGION_GENERATOR_LOC, manager, 16, 16, this.lodConfig.getLods().length + 1, params);
+
+ ShaderStorageBufferObject lodScales = this.regionTextureGenerator.createAndBindSSBO(LOD_SCALES_NAME, GL15.GL_STATIC_READ);
+ int lodScalesSize = this.lodConfig.getLods().length * 4 + 4;
+ lodScales.allocateBuffer(lodScalesSize);
+ lodScales.writeData(b -> {
+ b.putFloat(1.0F); // Primary chunk scale
+ for (LevelOfDetail l : this.lodConfig.getLods())
+ b.putFloat((float)l.chunkScale());
+ b.rewind();
+ }, lodScalesSize, false);
+
+ // Data for the cloud regions in world
+ this.regionTextureGenerator.createAndBindSSBO(CLOUD_REGIONS_NAME, GL15.GL_STATIC_READ).allocateBuffer(MAX_CLOUD_FORMATIONS * BYTES_PER_REGION);
+
+ // Create the cloud region 2D array texture
+
+ // Here we calculate the maximum size we need for this array texture,
+ // ensuring each block in the mesh will have a value to read in this texture
+ // when doing mesh generation
+ int prevSpan = this.lodConfig.getPrimaryChunkSpan();
+ int prevScale = 1;
+ int largestSpan = prevSpan;
+ for (LevelOfDetail config : this.lodConfig.getLods())
+ {
+ int scale = config.chunkScale();
+ int div = scale / prevScale;
+ prevScale = scale;
+ prevSpan = prevSpan / div + config.spread() * 2;
+ if (prevSpan > largestSpan)
+ largestSpan = prevSpan;
+ }
+ this.requiredRegionTexSize = largestSpan * SimpleCloudsConstants.CHUNK_SIZE;
+
+ if (this.cloudRegionTextureId != -1)
+ {
+ TextureUtil.releaseTextureId(this.cloudRegionTextureId);
+ this.cloudRegionTextureId = -1;
+ }
+
+ this.cloudRegionTextureId = TextureUtil.generateTextureId();
+ GL11.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.cloudRegionTextureId);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+ GL12.glTexImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, GL30.GL_RG32F, this.requiredRegionTexSize, this.requiredRegionTexSize, this.lodConfig.getLods().length + 1, 0, GL30.GL_RG, GL11.GL_FLOAT, (IntBuffer)null);
+ GL11.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, 0);
+
+ // Assign an image unit to it so any shader can access it
+ if (this.cloudRegionImageBinding != -1)
+ BindingManager.freeImageUnit(this.cloudRegionImageBinding);
+ this.cloudRegionImageBinding = BindingManager.getAvailableImageUnit();
+ BindingManager.useImageUnit(this.cloudRegionImageBinding);
+ GL42.glBindImageTexture(this.cloudRegionImageBinding, this.cloudRegionTextureId, 0, true, 0, GL15.GL_WRITE_ONLY, GL30.GL_RG32F);
+ this.regionTextureGenerator.setImageUnit("regionTexture", this.cloudRegionImageBinding);
+
+ this.runRegionGenerator(0.0F, 0.0F, 1.0F);
+
+ // Update the main mesh shader to use this texture
+ this.shader.setSampler2DArray("RegionsSampler", this.cloudRegionTextureId, 0);
+ this.shader.forUniform("RegionsTexSize", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.requiredRegionTexSize);
+ });
+
+ LOGGER.debug("Created cloud region texture generator with size {}x{}x{}", this.requiredRegionTexSize, this.requiredRegionTexSize, this.lodConfig.getLods().length + 1);
+ }
+
+ @Override
+ protected CloudMeshGenerator.ChunkGenSettings determineChunkGenSettings(float minX, float minZ, float maxX, float maxZ)
+ {
+ float[][] positions = new float[][] { {minX, minZ}, {minX, maxZ}, {maxX, minZ}, {maxX, maxZ} };
+ int smallestStartHeight = 0;
+ int largestEndHeight = 0;
+ boolean empty = true;
+ for (int i = 0; i < positions.length; i++)
+ {
+ float[] pos = positions[i];
+ Pair typeAt = this.cloudGetter.getCloudTypeAtPosition(pos[0], pos[1]);
+ if (typeAt.getRight() < 1.0F)
+ empty = false;
+ NoiseSettings config = typeAt.getLeft().noiseConfig();
+ int startHeight = config.getStartHeight();
+ int endHeight = config.getEndHeight();
+ if (i == 0 || smallestStartHeight > startHeight)
+ smallestStartHeight = startHeight;
+ if (i == 0 || largestEndHeight < endHeight)
+ largestEndHeight = endHeight;
+ }
+ if (empty || smallestStartHeight == largestEndHeight)
+ return skip();
+ else
+ return heights(smallestStartHeight, largestEndHeight);
+ }
+
+ @Override
+ protected void generateChunk(CloudMeshGenerator.ChunkGenTask task)
+ {
+ this.shader.forUniform("RegionSampleOffset", (id, loc) ->
+ {
+ PreparedChunk chunk = task.chunk().getChunkInfo();
+ GL41.glProgramUniform2f(id, loc, chunk.x() * (float)SimpleCloudsConstants.CHUNK_SIZE + (float)this.requiredRegionTexSize / 2.0F, chunk.z() * (float)SimpleCloudsConstants.CHUNK_SIZE + (float)this.requiredRegionTexSize / 2.0F);
+ });
+ this.shader.setSampler2DArray("RegionsSampler", this.cloudRegionTextureId, 0);
+
+ super.generateChunk(task);
+ }
+
+ private void runRegionGenerator(float meshOffsetX, float meshOffsetZ, float partialTick)
+ {
+ if (this.regionTextureGenerator == null || !this.regionTextureGenerator.isValid())
+ return;
+
+ this.uploadCloudRegionData(partialTick);
+ this.regionTextureGenerator.forUniform("Offset", (id, loc) -> {
+ GL41.glProgramUniform2f(id, loc, meshOffsetX, meshOffsetZ);
+ });
+ this.regionTextureGenerator.dispatchAndWait(this.requiredRegionTexSize / 16, this.requiredRegionTexSize / 16, 1);
+ }
+
+ private void uploadCloudRegionData(float partialTick)
+ {
+ if (this.regionTextureGenerator == null || !this.regionTextureGenerator.isValid())
+ return;
+
+ // Converts the cloud regions into data we can then easily pack into
+ // the SSBO. This method also checks and excludes cloud regions that
+ // reference cloud types that are not set up in this mesh generator
+ // to avoid errors.
+ List regionData = this.cloudGetter.getClouds().stream().map(region ->
+ {
+ Matrix2f transform = region.createTransform(partialTick);
+ float[] data = new float[] {
+ region.getPosX(partialTick),
+ region.getPosZ(partialTick),
+ (float)ArrayUtils.indexOf(this.cachedTypes, this.cloudGetter.getCloudTypeForId(region.getCloudTypeId())),
+ region.getRadius(partialTick),
+ transform.m00,
+ transform.m01,
+ transform.m10,
+ transform.m11
+ };
+ return data;
+ }).filter(data -> data[2] >= 0.0F).toList();
+
+ int regionDataSize = regionData.size();
+ int count = Math.min(MAX_CLOUD_FORMATIONS, regionDataSize);
+ if (regionDataSize != this.currentCloudFormationCount)
+ {
+ if (regionDataSize > MAX_CLOUD_FORMATIONS && regionDataSize > this.currentCloudFormationCount)
+ LOGGER.warn("Cloud formations {}/{}. Maximum count has been exceeded; some cloud formations will be ignored. Please ensure cloud formation count does not exceed the maximum of {}.", regionData.size(), MAX_CLOUD_FORMATIONS, MAX_CLOUD_FORMATIONS);
+ this.currentCloudFormationCount = regionDataSize;
+ }
+
+ if (count > 0)
+ {
+ ShaderStorageBufferObject regionsBuffer = this.regionTextureGenerator.getShaderStorageBuffer("CloudRegions");
+ regionsBuffer.writeData(b ->
+ {
+ for (int i = 0; i < count; i++)
+ {
+ float[] data = regionData.get(i);
+ for (float f : data)
+ b.putFloat(f);
+ }
+ b.rewind();
+ }, count * BYTES_PER_REGION, false);
+ }
+
+ this.regionTextureGenerator.forUniform("TotalCloudRegions", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, count);
+ });
+ }
+
+ private void uploadCloudTypeData()
+ {
+ RenderSystem.assertOnRenderThreadOrInit();
+
+ if (this.shader != null && this.shader.isValid())
+ {
+ var toCopy = this.cloudGetter.getIndexedCloudTypes();
+ if (toCopy.length > MAX_CLOUD_TYPES)
+ LOGGER.warn("Cloud type count exceeds the maximum. Not all cloud types will render.");
+ int copySize = Math.min(MAX_CLOUD_TYPES, toCopy.length);
+ this.cachedTypes = Arrays.copyOf(toCopy, copySize);
+
+ LOGGER.debug("Uploading cloud type noise data...");
+
+ this.shader.getShaderStorageBuffer(LAYER_GROUPINGS_NAME).writeData(b ->
+ {
+ int previousLayerIndex = 0;
+ for (int i = 0; i < this.cachedTypes.length; i++)
+ {
+ CloudInfo type = this.cachedTypes[i];
+ previousLayerIndex = type.packToBuffer(b, previousLayerIndex);
+ }
+ b.rewind();
+ }, CloudInfo.BYTES_PER_TYPE * this.cachedTypes.length, false);
+
+ this.shader.getShaderStorageBuffer(NOISE_LAYERS_NAME).writeData(b ->
+ {
+ for (int i = 0; i < this.cachedTypes.length; i++)
+ {
+ NoiseSettings settings = this.cachedTypes[i].noiseConfig();
+ float[] packed = settings.packForShader();
+ for (int j = 0; j < packed.length && j < AbstractNoiseSettings.Param.values().length * MAX_NOISE_LAYERS; j++)
+ b.putFloat(packed[j]);
+ }
+ b.rewind();
+ }, AbstractNoiseSettings.Param.values().length * 4 * MAX_NOISE_LAYERS * this.cachedTypes.length, false);
+ }
+ }
+
+ @Override
+ protected int prepareMeshGen(double originX, double originY, double originZ, float meshGenOffsetX, float meshGenOffsetZ, @Nullable Frustum frustum, int interval, float partialTick)
+ {
+ if (this.updateCloudTypes)
+ {
+ this.uploadCloudTypeData();
+ this.updateCloudTypes = false;
+ }
+
+ this.runRegionGenerator(meshGenOffsetX, meshGenOffsetZ, partialTick);
+
+ return super.prepareMeshGen(originX, originY, originZ, meshGenOffsetX, meshGenOffsetZ, frustum, interval, partialTick);
+ }
+
+ @Override
+ protected void onOffGen()
+ {
+ super.onOffGen();
+
+ if (this.regionTextureGenerator != null)
+ this.regionTextureGenerator.getShaderStorageBuffer("CloudRegions").readData(buf -> {}, BYTES_PER_REGION * MAX_CLOUD_FORMATIONS);
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+
+ this.currentCloudFormationCount = 0;
+ this.requiredRegionTexSize = 0;
+ this.updateCloudTypes = false;
+ this.cloudGetter = CloudGetter.EMPTY;
+ this.cachedTypes = new CloudInfo[0];
+
+ if (this.regionTextureGenerator != null)
+ {
+ this.regionTextureGenerator.close();
+ this.regionTextureGenerator = null;
+ }
+
+ if (this.cloudRegionTextureId != -1)
+ {
+ TextureUtil.releaseTextureId(this.cloudRegionTextureId);
+ this.cloudRegionTextureId = -1;
+ }
+
+ if (this.cloudRegionImageBinding != -1)
+ {
+ BindingManager.freeImageUnit(this.cloudRegionImageBinding);
+ this.cloudRegionImageBinding = -1;
+ }
+ }
+
+ @Override
+ public void fillReport(CrashReportCategory category)
+ {
+ category.setDetail("Cloud Types", "(" + this.cachedTypes.length + ") " + Joiner.on(", ").join(this.cachedTypes));
+ category.setDetail("Cloud Regions", this.cloudGetter.getClouds().size());
+ category.setDetail("Cloud Formations", this.currentCloudFormationCount);
+ super.fillReport(category);
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/client/mesh/instancing/InstanceableMesh.java b/dev/nonamecrackers2/simpleclouds/client/mesh/instancing/InstanceableMesh.java
new file mode 100644
index 00000000..49a38c40
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/client/mesh/instancing/InstanceableMesh.java
@@ -0,0 +1,219 @@
+package dev.nonamecrackers2.simpleclouds.client.mesh.instancing;
+
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL31;
+import org.lwjgl.system.MemoryUtil;
+
+import com.mojang.blaze3d.platform.MemoryTracker;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import com.mojang.blaze3d.vertex.VertexFormat;
+
+public class InstanceableMesh
+{
+ private int arrayObjectId = -1;
+ private int vertexBufferId = -1;
+ private int indexBufferId = -1;
+ private @Nullable ByteBuffer vertexBuffer;
+ private @Nullable ByteBuffer indexBuffer;
+ private int totalIndices;
+
+ public InstanceableMesh(int vertexBufferSize, int indexBufferSize, VertexFormat format, Consumer vertexBufferGenerator, Function indexBufferGenerator)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ this.arrayObjectId = GL30.glGenVertexArrays();
+ this.vertexBufferId = GL15.glGenBuffers();
+ this.indexBufferId = GL15.glGenBuffers();
+
+ GL30.glBindVertexArray(this.arrayObjectId);
+
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, this.vertexBufferId);
+ this.vertexBuffer = MemoryTracker.create(vertexBufferSize);
+ vertexBufferGenerator.accept(this.vertexBuffer);
+ GL15.glBufferData(GL15.GL_ARRAY_BUFFER, this.vertexBuffer, GL15.GL_STATIC_DRAW);
+ format.setupBufferState();
+ GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, this.indexBufferId);
+ this.indexBuffer = MemoryTracker.create(indexBufferSize);
+ this.totalIndices = indexBufferGenerator.apply(this.indexBuffer);
+ GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer, GL15.GL_STATIC_DRAW);
+
+ GL30.glBindVertexArray(0);
+ }
+
+ public static InstanceableMesh defaultSide()
+ {
+ return new InstanceableMesh(48, 24, DefaultVertexFormat.POSITION, buffer ->
+ {
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.rewind();
+ }, buffer ->
+ {
+ buffer.putInt(0);
+ buffer.putInt(1);
+ buffer.putInt(2);
+ buffer.putInt(0);
+ buffer.putInt(2);
+ buffer.putInt(3);
+ buffer.rewind();
+ return 6;
+ });
+ }
+
+ public static InstanceableMesh defaultNonCulledSide()
+ {
+ return new InstanceableMesh(48, 48, DefaultVertexFormat.POSITION, buffer ->
+ {
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.rewind();
+ }, buffer ->
+ {
+ buffer.putInt(0);
+ buffer.putInt(1);
+ buffer.putInt(2);
+ buffer.putInt(0);
+ buffer.putInt(2);
+ buffer.putInt(3);
+
+ buffer.putInt(2);
+ buffer.putInt(1);
+ buffer.putInt(0);
+ buffer.putInt(3);
+ buffer.putInt(2);
+ buffer.putInt(0);
+
+ buffer.rewind();
+ return 12;
+ });
+ }
+
+// public static PreparedMesh defaultCube()
+// {
+// return new PreparedMesh(576, 144, SimpleCloudsShaders.POSITION_NORMAL, buffer -> {
+// //-x
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// //+x
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// //-y
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// //+y
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// //-z
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// //-z
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+//
+// buffer.rewind();
+// }, buffer -> {
+// buffer.putInt(0); buffer.putInt(1); buffer.putInt(2); buffer.putInt(0); buffer.putInt(2); buffer.putInt(3); // -x
+// buffer.putInt(4); buffer.putInt(5); buffer.putInt(6); buffer.putInt(4); buffer.putInt(6); buffer.putInt(7); // +x
+// buffer.putInt(8); buffer.putInt(9); buffer.putInt(10); buffer.putInt(8); buffer.putInt(10); buffer.putInt(11); // -y
+// buffer.putInt(12); buffer.putInt(13); buffer.putInt(14); buffer.putInt(12); buffer.putInt(14); buffer.putInt(15); // +y
+// buffer.putInt(16); buffer.putInt(17); buffer.putInt(18); buffer.putInt(16); buffer.putInt(18); buffer.putInt(19); // -z
+// buffer.putInt(20); buffer.putInt(21); buffer.putInt(22); buffer.putInt(20); buffer.putInt(22); buffer.putInt(23); // +z
+// buffer.rewind();
+// return 36;
+// });
+// }
+
+ public static InstanceableMesh defaultCube()
+ {
+ return new InstanceableMesh(96, 144, DefaultVertexFormat.POSITION, buffer ->
+ {
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.rewind();
+ }, buffer ->
+ {
+ buffer.putInt(0); buffer.putInt(1); buffer.putInt(2); buffer.putInt(0); buffer.putInt(2); buffer.putInt(3); // -z
+ buffer.putInt(4); buffer.putInt(7); buffer.putInt(6); buffer.putInt(4); buffer.putInt(6); buffer.putInt(5); // +z
+ buffer.putInt(7); buffer.putInt(0); buffer.putInt(3); buffer.putInt(7); buffer.putInt(3); buffer.putInt(6); // -x
+ buffer.putInt(1); buffer.putInt(4); buffer.putInt(5); buffer.putInt(1); buffer.putInt(5); buffer.putInt(2); // +x
+ buffer.putInt(1); buffer.putInt(0); buffer.putInt(7); buffer.putInt(1); buffer.putInt(7); buffer.putInt(4); // -y
+ buffer.putInt(5); buffer.putInt(6); buffer.putInt(3); buffer.putInt(5); buffer.putInt(3); buffer.putInt(2); // +y
+ buffer.rewind();
+ return 36;
+ });
+ }
+
+ public void drawInstanced(int count)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ GL30.glBindVertexArray(this.arrayObjectId);
+ GL31.glDrawElementsInstanced(GL11.GL_TRIANGLES, this.totalIndices, GL11.GL_UNSIGNED_INT, 0L, count);
+ }
+
+ public void destroy()
+ {
+ this.totalIndices = 0;
+
+ if (this.arrayObjectId >= 0)
+ {
+ RenderSystem.glDeleteVertexArrays(this.arrayObjectId);
+ this.arrayObjectId = -1;
+ }
+
+ if (this.vertexBufferId >= 0)
+ {
+ RenderSystem.glDeleteBuffers(this.vertexBufferId);
+ this.vertexBufferId = -1;
+ }
+
+ if (this.vertexBuffer != null)
+ {
+ MemoryUtil.memFree(this.vertexBuffer);
+ this.vertexBuffer = null;
+ }
+
+ if (this.indexBufferId >= 0)
+ {
+ RenderSystem.glDeleteBuffers(this.indexBufferId);
+ this.indexBufferId = -1;
+ }
+
+ if (this.indexBuffer != null)
+ {
+ MemoryUtil.memFree(this.indexBuffer);
+ this.indexBuffer = null;
+ }
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer.java b/dev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer.java
new file mode 100644
index 00000000..7bd37397
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer.java
@@ -0,0 +1,1497 @@
+package dev.nonamecrackers2.simpleclouds.client.renderer;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+import org.joml.Matrix4f;
+import org.joml.Vector2f;
+import org.joml.Vector3f;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL14;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL40;
+import org.lwjgl.opengl.GL43;
+
+import com.google.common.collect.Lists;
+import com.google.gson.JsonSyntaxException;
+import com.mojang.blaze3d.pipeline.RenderTarget;
+import com.mojang.blaze3d.pipeline.TextureTarget;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.platform.Window;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.BufferBuilder;
+import com.mojang.blaze3d.vertex.BufferUploader;
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.Tesselator;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+import com.mojang.blaze3d.vertex.VertexFormat;
+import com.mojang.math.Axis;
+
+import dev.nonamecrackers2.simpleclouds.SimpleCloudsMod;
+import dev.nonamecrackers2.simpleclouds.api.client.event.ModifyCloudRenderDistanceEvent;
+import dev.nonamecrackers2.simpleclouds.api.common.cloud.CloudMode;
+import dev.nonamecrackers2.simpleclouds.client.cloud.ClientSideCloudTypeManager;
+import dev.nonamecrackers2.simpleclouds.client.compat.SimpleCloudsCompatHelper;
+import dev.nonamecrackers2.simpleclouds.client.event.impl.DetermineCloudRenderPipelineEvent;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.CloudRenderTarget;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.ShadowMapBuffer;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.WeightedBlendingTarget;
+import dev.nonamecrackers2.simpleclouds.client.mesh.RendererInitializeResult;
+import dev.nonamecrackers2.simpleclouds.client.mesh.chunk.MeshChunk;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.CloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.MultiRegionCloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.SingleRegionCloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetailConfig;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.PreparedChunk;
+import dev.nonamecrackers2.simpleclouds.client.renderer.lightning.LightningBolt;
+import dev.nonamecrackers2.simpleclouds.client.renderer.pipeline.CloudsRenderPipeline;
+import dev.nonamecrackers2.simpleclouds.client.renderer.settings.CloudsRendererSettings;
+import dev.nonamecrackers2.simpleclouds.client.shader.SimpleCloudsShaders;
+import dev.nonamecrackers2.simpleclouds.client.shader.SingleSSBOShaderInstance;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.BindingManager;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.ShaderStorageBufferObject;
+import dev.nonamecrackers2.simpleclouds.client.world.ClientCloudManager;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudGetter;
+import dev.nonamecrackers2.simpleclouds.common.config.SimpleCloudsConfig;
+import dev.nonamecrackers2.simpleclouds.mixin.MixinPostChain;
+import net.minecraft.CrashReport;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.renderer.EffectInstance;
+import net.minecraft.client.renderer.GameRenderer;
+import net.minecraft.client.renderer.LevelRenderer;
+import net.minecraft.client.renderer.LightTexture;
+import net.minecraft.client.renderer.PostChain;
+import net.minecraft.client.renderer.PostPass;
+import net.minecraft.client.renderer.ShaderInstance;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.client.renderer.texture.AbstractTexture;
+import net.minecraft.client.renderer.texture.TextureManager;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
+import net.minecraft.util.FastColor;
+import net.minecraft.util.Mth;
+import net.minecraft.util.profiling.ProfilerFiller;
+import net.minecraft.world.effect.MobEffectInstance;
+import net.minecraft.world.effect.MobEffects;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.phys.Vec3;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.StartupMessageManager;
+import net.minecraftforge.fml.loading.ImmediateWindowHandler;
+import nonamecrackers2.crackerslib.common.compat.CompatHelper;
+
+public class SimpleCloudsRenderer implements ResourceManagerReloadListener
+{
+ private static final Logger LOGGER = LogManager.getLogger("simpleclouds/SimpleCloudsRenderer");
+ private static final Vector3f DIFFUSE_LIGHT_0 = (new Vector3f(0.2F, 1.0F, -0.7F)).normalize();
+ private static final Vector3f DIFFUSE_LIGHT_1 = (new Vector3f(-0.2F, 1.0F, 0.7F)).normalize();
+ private static final ResourceLocation STORM_POST_PROCESSING_LOC = SimpleCloudsMod.id("shaders/post/storm_post.json");
+ private static final ResourceLocation BLUR_POST_PROCESSING_LOC = SimpleCloudsMod.id("shaders/post/blur_post.json");
+ private static final ResourceLocation SCREEN_SPACE_WORLD_FOG_LOC = SimpleCloudsMod.id("shaders/post/screen_space_world_fog.json");
+ private static final ResourceLocation CLOUD_SHADOWS_LOC = SimpleCloudsMod.id("shaders/post/cloud_shadows.json");
+ public static final ResourceLocation FINAL_COMPOSITE_LOC = SimpleCloudsMod.id("shaders/post/final_composite.json");
+ public static final ResourceLocation FINAL_COMPOSITE_NO_TRANSPARENCY_LOC = SimpleCloudsMod.id("shaders/post/final_composite_no_transparency.json");
+ private static final ResourceLocation DITHER_TEXTURE = SimpleCloudsMod.id("textures/shader/bayer_matrix.png");
+ private static final ArtifactVersion REQUIRED_OPENGL_VERSION = new DefaultArtifactVersion("4.3");
+ public static final int SHADOW_MAP_SIZE = 1024;
+ public static final int SHADOW_MAP_SPAN = 10000;
+ public static final int MAX_LIGHTNING_BOLTS = 16;
+ public static final int BYTES_PER_LIGHTNING_BOLT = 16;
+ public static final float CHUNK_FADE_IN_ALPHA_PER_TICK = 0.2F;
+ public static final float DITHER_SCALE = 0.05F;
+ private static @Nullable SimpleCloudsRenderer instance;
+ private final CloudsRendererSettings settings;
+ private final Minecraft mc;
+ private final WorldEffects worldEffectsManager;
+ private final AtmosphericCloudsRenderHandler atmoshpericClouds;
+ private @Nullable ClientCloudManager cloudManager;
+ private ArtifactVersion openGlVersion;
+ private CloudMeshGenerator meshGenerator;
+ private @Nullable CloudsRenderPipeline renderPipelineThisPass;
+ private @Nullable RenderTarget cloudTarget;
+ private @Nullable WeightedBlendingTarget cloudTransparencyTarget;
+ private @Nullable RenderTarget stormFogTarget;
+ private int stormFogResolutionDivisor = 4;
+ private @Nullable RenderTarget blurTarget;
+ private final List postChains = Lists.newArrayList();
+ private @Nullable PostChain finalComposite;
+ private @Nullable PostChain stormPostProcessing;
+ private @Nullable PostChain blurPostProcessing;
+ private @Nullable PostChain screenSpaceWorldFog;
+ private @Nullable PostChain cloudShadows;
+ private @Nullable ShaderStorageBufferObject lightningBoltPositions;
+ private @Nullable ShadowMapBuffer stormFogShadowMap;
+ private Optional shadowMap = Optional.empty();
+ private @Nullable Frustum cullFrustum;
+ private float fogStart;
+ private float fogEnd;
+ private @Nullable PoseStack stormFogShadowMapStack;
+ private @Nullable PoseStack shadowMapStack;
+ private boolean failedToCopyDepthBuffer;
+ private boolean needsReload;
+ private @Nullable RendererInitializeResult initialInitializationResult;
+
+ private SimpleCloudsRenderer(CloudsRendererSettings settings, Minecraft mc)
+ {
+ this.settings = settings;
+ this.mc = mc;
+ this.worldEffectsManager = new WorldEffects(mc, this);
+ this.atmoshpericClouds = new AtmosphericCloudsRenderHandler(mc);
+ }
+
+ public String getClientCloudManagerString()
+ {
+ return this.cloudManager != null ? this.cloudManager.toString() : "null";
+ }
+
+ public CloudMeshGenerator getMeshGenerator()
+ {
+ return this.meshGenerator;
+ }
+
+ public CloudsRenderPipeline getRenderPipeline()
+ {
+ return Objects.requireNonNull(this.renderPipelineThisPass, "Pipeline not determined");
+ }
+
+ public WorldEffects getWorldEffectsManager()
+ {
+ return this.worldEffectsManager;
+ }
+
+ public AtmosphericCloudsRenderHandler getAtmosphericCloudRenderer()
+ {
+ return this.atmoshpericClouds;
+ }
+
+ public CloudsRendererSettings getSettings()
+ {
+ return this.settings;
+ }
+
+ public @Nullable RendererInitializeResult getInitialInitializationResult()
+ {
+ return this.initialInitializationResult;
+ }
+
+ public ShadowMapBuffer getStormFogShadowMap()
+ {
+ return this.stormFogShadowMap;
+ }
+
+ public Optional getShadowMap()
+ {
+ return this.shadowMap;
+ }
+
+ public @Nullable PoseStack getStormFogShadowMapStack()
+ {
+ return this.stormFogShadowMapStack;
+ }
+
+ public @Nullable PoseStack getShadowMapStack()
+ {
+ return this.shadowMapStack;
+ }
+
+ public RenderTarget getBlurTarget()
+ {
+ return this.blurTarget;
+ }
+
+ public RenderTarget getStormFogTarget()
+ {
+ return this.stormFogTarget;
+ }
+
+ public RenderTarget getCloudTarget()
+ {
+ return this.cloudTarget;
+ }
+
+ public WeightedBlendingTarget getCloudTransparencyTarget()
+ {
+ return this.cloudTransparencyTarget;
+ }
+
+ public float getFogStart()
+ {
+ return this.fogStart;
+ }
+
+ public float getFogEnd()
+ {
+ return this.fogEnd;
+ }
+
+ public float getFadeFactorForDistance(float distance)
+ {
+ return 1.0F - Math.min(Math.max(distance - this.fogStart, 0.0F) / (this.fogEnd - this.fogStart), 1.0F);
+ }
+
+ public @Nullable Frustum getCullFrustum()
+ {
+ return this.cullFrustum;
+ }
+
+ public void onCloudManagerChange(ClientCloudManager manager)
+ {
+ this.cloudManager = manager;
+ if (this.meshGenerator instanceof MultiRegionCloudMeshGenerator generator)
+ generator.setCloudGetter(manager);
+ }
+
+ private void prepareMeshGenerator(float partialTicks)
+ {
+ if (this.meshGenerator instanceof SingleRegionCloudMeshGenerator generator)
+ generator.setFadeDistances((float)SimpleCloudsConfig.CLIENT.singleModeFadeStartPercentage.get() / 100.0F, (float)SimpleCloudsConfig.CLIENT.singleModeFadeEndPercentage.get() / 100.0F);
+ this.meshGenerator.setTransparencyRenderDistance((float)SimpleCloudsConfig.CLIENT.transparencyRenderDistancePercentage.get() / 100.0F);
+ this.meshGenerator.setTestFacesFacingAway(SimpleCloudsConfig.CLIENT.testSidesThatAreOccluded.get());
+ if (this.mc.level != null)
+ {
+ this.meshGenerator.setScroll(this.cloudManager.getScrollX(partialTicks), this.cloudManager.getScrollY(partialTicks), this.cloudManager.getScrollZ(partialTicks));
+ }
+ }
+
+ public boolean needsReinitialization()
+ {
+ return this.settings.needsReinitialization(this.meshGenerator);
+ }
+
+ public void requestReload()
+ {
+ LOGGER.debug("Requesting reload...");
+ this.needsReload = true;
+ }
+
+ @Override
+ public void onResourceManagerReload(ResourceManager manager)
+ {
+ RenderSystem.assertOnRenderThreadOrInit();
+
+ this.initialInitializationResult = null;
+
+ // --- Check OpenGL version ---
+
+ ArtifactVersion openGlVersion = this.openGlVersion;
+ if (openGlVersion == null)
+ openGlVersion = new DefaultArtifactVersion(ImmediateWindowHandler.getGLVersion());
+ if (openGlVersion.compareTo(REQUIRED_OPENGL_VERSION) < 0)
+ {
+ LOGGER.error("Simple Clouds renderer could not initialize. OpenGL version is {}, minimum required is {}", openGlVersion, REQUIRED_OPENGL_VERSION);
+ this.initialInitializationResult = RendererInitializeResult.builder().errorOpenGL().build();
+ this.openGlVersion = openGlVersion;
+ return;
+ }
+
+ if (!SimpleCloudsShaders.areShadersInitialized())
+ {
+ LOGGER.error("Simple Clouds renderer could not initialize. Core shaders are not initialized.");
+ this.initialInitializationResult = RendererInitializeResult.builder().coreShadersNotInitialized(SimpleCloudsShaders.getError()).build();
+ saveAndPrintCrashReports(this.mc, this.initialInitializationResult);
+ return;
+ }
+
+ RendererInitializeResult compatError = SimpleCloudsCompatHelper.findCompatErrors();
+ if (compatError.getState() == RendererInitializeResult.State.ERROR)
+ {
+ LOGGER.error("Simple Clouds renderer could not initialize due to compat error(s): {}", compatError.getErrors().stream().map(e -> e.text().getString()).toList());
+ this.initialInitializationResult = compatError;
+ saveAndPrintCrashReports(this.mc, this.initialInitializationResult);
+ return;
+ }
+
+ StartupMessageManager.addModMessage("Initializing Simple Clouds renderer");
+
+ LOGGER.debug("OpenGL {}", openGlVersion);
+
+ Instant started = Instant.now();
+
+ LOGGER.debug("Beginning Simple Clouds renderer initialization");
+
+ this.failedToCopyDepthBuffer = false;
+
+ // --- Render Targets ---
+
+ boolean highPrecisionDepth = SimpleCloudsMod.dhLoaded();
+
+ RenderTarget main = SimpleCloudsCompatHelper.getMainRenderTarget();
+ if (main == null)
+ {
+ this.initialInitializationResult = RendererInitializeResult.builder().errorUnknown(new NullPointerException("Main framebuffer is null"), "Simple Clouds Renderer").build();
+ saveAndPrintCrashReports(this.mc, this.initialInitializationResult);
+ return;
+ }
+
+ if (this.cloudTarget != null)
+ this.cloudTarget.destroyBuffers();
+ this.cloudTarget = new CloudRenderTarget(main.width, main.height, Minecraft.ON_OSX, highPrecisionDepth);
+ this.cloudTarget.setClearColor(0.0F, 0.0F, 0.0F, 0.0F);
+
+ if (this.cloudTransparencyTarget != null)
+ this.cloudTransparencyTarget.destroyBuffers();
+ this.cloudTransparencyTarget = new WeightedBlendingTarget(main.width, main.height, Minecraft.ON_OSX, highPrecisionDepth);
+
+ this.stormFogResolutionDivisor = SimpleCloudsCompatHelper.getStormFogResolutionDivisor();
+ if (this.stormFogTarget != null)
+ this.stormFogTarget.destroyBuffers();
+ this.stormFogTarget = new TextureTarget(main.width / this.stormFogResolutionDivisor, main.height / this.stormFogResolutionDivisor, false, Minecraft.ON_OSX);
+ this.stormFogTarget.setClearColor(0.0F, 0.0F, 0.0F, 0.0F);
+ this.stormFogTarget.setFilterMode(GL11.GL_LINEAR);
+
+ if (this.blurTarget != null)
+ this.blurTarget.destroyBuffers();
+ this.blurTarget = new TextureTarget(main.width, main.height, false, Minecraft.ON_OSX);
+ this.blurTarget.setClearColor(0.0F, 0.0F, 0.0F, 0.0F);
+ this.blurTarget.setFilterMode(GL11.GL_LINEAR);
+
+ // --- Mesh Generator ---
+
+ this.setupMeshGenerator(); // Create/setup the generator
+ this.prepareMeshGenerator(0.0F); // Prepare it
+
+ RendererInitializeResult result = this.meshGenerator.init(manager); // Initialize
+ if (this.initialInitializationResult == null)
+ this.initialInitializationResult = result;
+
+ // --- Shadow Map ---
+
+ if (this.stormFogShadowMap != null)
+ {
+ this.stormFogShadowMap.close();
+ this.stormFogShadowMap = null;
+ }
+
+ this.shadowMap.ifPresent(buffer -> {
+ buffer.close();
+ });
+
+ int span = this.meshGenerator.getLodConfig().getEffectiveChunkSpan() * SimpleCloudsConstants.CHUNK_SIZE * SimpleCloudsConstants.CLOUD_SCALE;
+ this.stormFogShadowMap = new ShadowMapBuffer(span, span, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0.0F, 10000.0F, true, false);
+
+ if (SimpleCloudsConfig.CLIENT.distantShadows.get() && SimpleCloudsMod.dhLoaded())
+ {
+ int distantShadowSpan = SimpleCloudsConfig.CLIENT.shadowDistance.get() * 2;
+ distantShadowSpan = Math.min(distantShadowSpan, span);
+ this.shadowMap = Optional.of(new ShadowMapBuffer(distantShadowSpan, distantShadowSpan, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0.0F, 10000.0F, false, true));
+ }
+ else
+ {
+ this.shadowMap = Optional.empty();
+ }
+
+ // --- Post Processing Shaders ---
+
+ this.destroyPostChains();
+
+ if (this.lightningBoltPositions != null)
+ {
+ BindingManager.freeSSBO(this.lightningBoltPositions);
+ this.lightningBoltPositions = null;
+ }
+
+ this.lightningBoltPositions = BindingManager.createSSBO(GL15.GL_DYNAMIC_DRAW);
+ this.lightningBoltPositions.allocateBuffer(MAX_LIGHTNING_BOLTS * BYTES_PER_LIGHTNING_BOLT);
+
+ this.stormPostProcessing = this.createPostChain(manager, STORM_POST_PROCESSING_LOC, this.stormFogTarget, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("ShadowMap", () -> this.stormFogShadowMap.getDepthTexId());
+ effect.setSampler("ShadowMapColor", () -> this.stormFogShadowMap.getColorTexId());
+ effect.setSampler("DepthSampler", () -> this.cloudTarget.getDepthTextureId());
+ this.lightningBoltPositions.optionalBindToProgram("LightningBolts", effect.getId());
+ });
+
+ this.blurPostProcessing = this.createPostChain(manager, BLUR_POST_PROCESSING_LOC, this.blurTarget);
+ this.blurPostProcessing.getTempTarget("swap").setFilterMode(GL11.GL_LINEAR);
+
+ this.screenSpaceWorldFog = this.createPostChain(manager, SCREEN_SPACE_WORLD_FOG_LOC, main, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("StormFogSampler", () -> this.blurTarget.getColorTextureId());
+ effect.setSampler("CloudDepthSampler", () -> this.cloudTarget.getDepthTextureId());
+ });
+
+ this.finalComposite = this.createPostChain(manager, this.settings.useTransparency() ? FINAL_COMPOSITE_LOC : FINAL_COMPOSITE_NO_TRANSPARENCY_LOC, main, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ if (this.settings.useTransparency())
+ {
+ effect.setSampler("AccumTexture", () -> this.cloudTransparencyTarget.getColorTextureId());
+ effect.setSampler("RevealageTexture", () -> this.cloudTransparencyTarget.getRevealageTextureId());
+ }
+ effect.setSampler("CloudsTexture", () -> this.cloudTarget.getColorTextureId());
+ });
+
+ if (this.shadowMap.isPresent())
+ {
+ ShadowMapBuffer map = this.shadowMap.get();
+ this.cloudShadows = this.createPostChain(manager, CLOUD_SHADOWS_LOC, main, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("ShadowMap", () -> this.shadowMap.get().getDepthTexId());
+ effect.safeGetUniform("ShadowSpan").set((float)Math.min(map.getViewWidth(), map.getViewHeight()));
+ });
+ }
+
+ this.atmoshpericClouds.init(manager);
+
+ // --- Final debug ---
+
+ long duration = Duration.between(started, Instant.now()).toMillis();
+ LOGGER.info("Finished initialization, took {} ms", duration);
+
+ LOGGER.debug("Total LODs: {}", this.meshGenerator.getLodConfig().getLods().length + 1);
+ LOGGER.debug("Highest detail (primary) chunk span: {}", this.meshGenerator.getLodConfig().getPrimaryChunkSpan());
+ LOGGER.debug("Effective chunk span with LODs (total viewable area): {}", this.meshGenerator.getLodConfig().getEffectiveChunkSpan());
+ LOGGER.debug("Total span in blocks: {}", this.meshGenerator.getLodConfig().getEffectiveChunkSpan() * SimpleCloudsConstants.CHUNK_SIZE * SimpleCloudsConstants.CLOUD_SCALE);
+
+ //Print crash reports if needed
+ saveAndPrintCrashReports(this.mc, result);
+ }
+
+ private static void saveAndPrintCrashReports(Minecraft mc, RendererInitializeResult result)
+ {
+ switch (result.getState())
+ {
+ case ERROR:
+ {
+ List reports = result.createCrashReports();
+ LOGGER.error("---------CRASH REPORT BEGIN---------");
+ for (CrashReport report : reports)
+ {
+ mc.fillReport(report);
+ LOGGER.error("{}", report.getFriendlyReport());
+ }
+ LOGGER.error("---------CRASH REPORT END---------");
+ result.saveCrashReports(mc.gameDirectory);
+ break;
+ }
+ default:
+ }
+ }
+
+ private void setupMeshGenerator()
+ {
+ if (this.settings.checkAndOrBeginInitialization(this.meshGenerator))
+ {
+ if (this.meshGenerator != null)
+ {
+ this.meshGenerator.close(); //Close the current generator
+ this.meshGenerator = null;
+ }
+
+ CloudMode mode = this.settings.getCurrentCloudMode();
+ boolean isAmbientMode = mode == CloudMode.AMBIENT;
+ boolean useMultiRegion = isAmbientMode || mode == CloudMode.DEFAULT;
+ boolean shadedClouds = this.settings.shadedClouds();
+ boolean useFixedMeshDataSectionSize = this.settings.useFixedMeshDataSectionSize();
+ boolean useTransparency = this.settings.useTransparency();
+ LevelOfDetailConfig lod = this.settings.getCurrentLod().getConfig();
+
+ var builder = CloudMeshGenerator.builder()
+ .fadeNearOrigin(isAmbientMode)
+ .shadedClouds(shadedClouds)
+ .fixedMeshDataSectionSize(useFixedMeshDataSectionSize)
+ .meshGenInterval(SimpleCloudsRenderer::calculateMeshGenInterval)
+ .lodConfig(lod)
+ .useTransparency(useTransparency);
+
+ if (useMultiRegion) //Use the multi-region generator for DEFAULT or AMBIENT cloud mode
+ {
+ if (isAmbientMode)
+ {
+ builder.fadeStart(SimpleCloudsConstants.AMBIENT_MODE_FADE_START)
+ .fadeEnd(SimpleCloudsConstants.AMBIENT_MODE_FADE_END);
+ }
+ this.meshGenerator = builder.createMultiRegion();
+ }
+ else if (mode == CloudMode.SINGLE)
+ {
+ float fadeStart = (float)SimpleCloudsConfig.CLIENT.singleModeFadeStartPercentage.get() / 100.0F;
+ float fadeEnd = (float)SimpleCloudsConfig.CLIENT.singleModeFadeEndPercentage.get() / 100.0F;
+ this.meshGenerator = builder.fadeStart(fadeStart).fadeEnd(fadeEnd).createSingleRegion(SimpleCloudsConstants.EMPTY);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Not sure how to handle cloud mode " + mode);
+ }
+ }
+
+ if (this.meshGenerator instanceof MultiRegionCloudMeshGenerator multiRegionGenerator)
+ {
+ multiRegionGenerator.setCloudGetter(this.cloudManager != null ? this.cloudManager : CloudGetter.EMPTY);
+ }
+ else if (this.meshGenerator instanceof SingleRegionCloudMeshGenerator singleRegionGenerator)
+ {
+ //Find the desired single mode cloud type, either from the client-side only context or
+ //from the synced cloud types from the server
+ CloudType type = this.settings.getSingleModeCloudType();
+ if (!ClientCloudManager.isAvailableServerSide() && !ClientSideCloudTypeManager.isValidClientSideSingleModeCloudType(type))
+ type = SimpleCloudsConstants.EMPTY;
+ if (type == null)
+ type = SimpleCloudsConstants.EMPTY;
+ singleRegionGenerator.setCloudType(type);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Not sure how to handle generator: " + this.meshGenerator);
+ }
+ }
+
+ private void destroyPostChains()
+ {
+ this.postChains.forEach(PostChain::close);
+ this.postChains.clear();
+ }
+
+ private @Nullable PostChain createPostChain(ResourceManager manager, ResourceLocation loc, RenderTarget target)
+ {
+ return this.createPostChain(manager, loc, target, effect -> {});
+ }
+
+ private @Nullable PostChain createPostChain(ResourceManager manager, ResourceLocation loc, RenderTarget target, Consumer passConsumer)
+ {
+ try
+ {
+ PostChain chain = new PostChain(this.mc.getTextureManager(), manager, target, loc);
+ chain.resize(target.width, target.height);
+ for (PostPass pass : ((MixinPostChain)chain).simpleclouds$getPostPasses())
+ passConsumer.accept(pass);
+ this.postChains.add(chain);
+ return chain;
+ }
+ catch (JsonSyntaxException e)
+ {
+ LOGGER.warn("Failed to parse post shader: {}", loc, e);
+ }
+ catch (IOException e)
+ {
+ LOGGER.warn("Failed to load post shader: {}", loc, e);
+ }
+
+ return null;
+ }
+
+ public void onMainWindowResize(int width, int height)
+ {
+ this.atmoshpericClouds.onResize(width, height);
+
+ RenderTarget main = SimpleCloudsCompatHelper.getMainRenderTarget();
+ if (main == null)
+ return;
+
+ width = main.width;
+ height = main.height;
+
+ if (this.cloudTarget != null)
+ this.cloudTarget.resize(width, height, Minecraft.ON_OSX);
+
+ if (this.cloudTransparencyTarget != null)
+ this.cloudTransparencyTarget.resize(width, height, Minecraft.ON_OSX);
+
+ this.stormFogResolutionDivisor = SimpleCloudsCompatHelper.getStormFogResolutionDivisor();
+
+ if (this.stormFogTarget != null)
+ {
+ this.stormFogTarget.resize(width / this.stormFogResolutionDivisor, height / this.stormFogResolutionDivisor, Minecraft.ON_OSX);
+ this.stormFogTarget.setFilterMode(GL11.GL_LINEAR);
+ }
+
+ if (this.blurTarget != null)
+ {
+ this.blurTarget.resize(width, height, Minecraft.ON_OSX);
+ this.blurTarget.setFilterMode(GL11.GL_LINEAR);
+ }
+
+ for (PostChain chain : this.postChains)
+ {
+ RenderTarget chainTarget = ((MixinPostChain)chain).simpleclouds$getScreenTarget();
+ chain.resize(chainTarget.width, chainTarget.height);
+ }
+
+ if (this.blurPostProcessing != null)
+ this.blurPostProcessing.getTempTarget("swap").setFilterMode(GL11.GL_LINEAR);
+ }
+
+ public void shutdown()
+ {
+ if (this.cloudTarget != null)
+ this.cloudTarget.destroyBuffers();
+ if (this.cloudTransparencyTarget != null)
+ this.cloudTransparencyTarget.destroyBuffers();
+ if (this.stormFogTarget != null)
+ this.stormFogTarget.destroyBuffers();;
+ if (this.blurTarget != null)
+ this.blurTarget.destroyBuffers();
+
+ this.cloudTarget = null;
+ this.cloudTransparencyTarget = null;
+ this.stormFogTarget = null;
+ this.blurTarget = null;
+
+ this.destroyPostChains();
+
+ if (this.meshGenerator != null)
+ this.meshGenerator.close();
+
+ if (this.stormFogShadowMap != null)
+ {
+ this.stormFogShadowMap.close();
+ this.stormFogShadowMap = null;
+ }
+
+ if (this.shadowMap.isPresent())
+ {
+ this.shadowMap.get().close();
+ this.shadowMap = Optional.empty();
+ }
+
+ if (this.lightningBoltPositions != null)
+ {
+ BindingManager.freeSSBO(this.lightningBoltPositions);
+ this.lightningBoltPositions = null;
+ }
+
+ this.atmoshpericClouds.close();
+ }
+
+ public void baseTick()
+ {
+ if (this.needsReload)
+ {
+ this.onResourceManagerReload(this.mc.getResourceManager());
+ this.needsReload = false;
+ }
+ }
+
+ public void tick()
+ {
+ this.worldEffectsManager.tick();
+
+ if (this.cloudManager != null)
+ this.atmoshpericClouds.setWindDirection(this.cloudManager.calculateWindDirection());
+ this.atmoshpericClouds.tick();
+
+ if (this.meshGenerator != null)
+ this.meshGenerator.worldTick();
+ }
+
+ public static void renderCloudsOpaque(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum)
+ {
+ renderCloudsOpaque(generator, stack, projMat, fogStart, fogEnd, partialTick, r, g, b, frustum, true);
+ }
+
+ public static void renderCloudsOpaque(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum, boolean ditherFade)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ BufferUploader.reset();
+
+ if (!generator.canRender())
+ return;
+
+ RenderSystem.disableBlend();
+ RenderSystem.enableDepthTest();
+ RenderSystem.disableCull();
+
+ SingleSSBOShaderInstance shader = SimpleCloudsShaders.getCloudsShader();
+ RenderSystem.setShader(() -> shader);
+
+ TextureManager manager = Minecraft.getInstance().getTextureManager();
+ AbstractTexture ditherTexture = manager.getTexture(DITHER_TEXTURE);
+ shader.setSampler("BayerMatrixSampler", ditherTexture);
+ shader.safeGetUniform("DitherScale").set(DITHER_SCALE);
+
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+ shader.apply();
+
+ generator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, opaqueBuffers) ->
+ {
+ if (ditherFade)
+ {
+ RenderSystem.setShaderColor(r, g, b, chunk.getAlpha(partialTick));
+ shader.COLOR_MODULATOR.set(RenderSystem.getShaderColor());
+ shader.COLOR_MODULATOR.upload();
+ }
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), opaqueBuffers.getBufferId());
+ generator.getSideMesh().drawInstanced(opaqueBuffers.getElementCount());
+ }, ditherFade);
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), 0);
+
+ shader.clear();
+
+ GL30.glBindVertexArray(0);
+
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ RenderSystem.enableCull();
+ }
+
+ public static void renderCloudsTransparency(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum)
+ {
+ renderCloudsTransparency(generator, stack, projMat, fogStart, fogEnd, partialTick, r, g, b, frustum, true);
+ }
+
+ public static void renderCloudsTransparency(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum, boolean ditherFade)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ BufferUploader.reset();
+
+ if (!generator.canRender() || !generator.transparencyEnabled())
+ return;
+
+ RenderSystem.enableDepthTest();
+ RenderSystem.depthMask(false);
+
+ SingleSSBOShaderInstance shader = SimpleCloudsShaders.getCloudsTransparencyShader();
+ RenderSystem.setShader(() -> shader);
+
+ TextureManager manager = Minecraft.getInstance().getTextureManager();
+ AbstractTexture ditherTexture = manager.getTexture(DITHER_TEXTURE);
+ shader.setSampler("BayerMatrixSampler", ditherTexture);
+ shader.safeGetUniform("DitherScale").set(DITHER_SCALE);
+
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+
+ shader.apply();
+
+ GL30.glEnablei(GL11.GL_BLEND, 0);
+ GL30.glEnablei(GL11.GL_BLEND, 1);
+ GL40.glBlendEquationi(0, GL14.GL_FUNC_ADD);
+ GL40.glBlendEquationi(1, GL14.GL_FUNC_ADD);
+ GL40.glBlendFunci(0, GL11.GL_ONE, GL11.GL_ONE);
+ GL40.glBlendFunci(1, GL11.GL_ZERO, GL11.GL_ONE_MINUS_SRC_COLOR);
+
+ generator.forRenderableMeshChunks(frustum, c -> c.getTransparentBuffers().get(), (chunk, transparentBuffers) ->
+ {
+ if (ditherFade)
+ {
+ RenderSystem.setShaderColor(r, g, b, chunk.getAlpha(partialTick));
+ shader.COLOR_MODULATOR.set(RenderSystem.getShaderColor());
+ shader.COLOR_MODULATOR.upload();
+ }
+
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), transparentBuffers.getBufferId());
+ generator.getCubeMesh().drawInstanced(transparentBuffers.getElementCount());
+ }, ditherFade);
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), 0);
+
+ shader.clear();
+
+ GL30.glDisablei(GL11.GL_BLEND, 0);
+ GL30.glDisablei(GL11.GL_BLEND, 1);
+ GL40.glBlendFuncSeparatei(0, GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
+ GL40.glBlendFuncSeparatei(1, GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
+
+ GL30.glBindVertexArray(0);
+
+ RenderSystem.depthMask(true);
+
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ }
+
+ private PoseStack createShadowMapStack(ShadowMapBuffer shadowMap, double camX, double camY, double camZ, Consumer transformApplier)
+ {
+ PoseStack stack = new PoseStack();
+ stack.setIdentity();
+ double depthCenter = ((double)shadowMap.getNear() + (double)shadowMap.getFar()) * -0.5D;
+ stack.translate((double)shadowMap.getViewWidth() / 2.0D, (double)shadowMap.getViewHeight() / 2.0D, depthCenter);
+ transformApplier.accept(stack);
+ float chunkSizeUpscaled = (float)SimpleCloudsConstants.CHUNK_SIZE * (float)SimpleCloudsConstants.CLOUD_SCALE;
+ float camOffsetX = ((float)Mth.floor(camX / chunkSizeUpscaled) * chunkSizeUpscaled);
+ float camOffsetZ = ((float)Mth.floor(camZ / chunkSizeUpscaled) * chunkSizeUpscaled);
+ stack.translate(-camOffsetX, -(double)this.cloudManager.getCloudHeight(), -camOffsetZ);
+ return stack;
+ }
+
+ private void renderShadowMap(ShadowMapBuffer shadowMap, PoseStack stack, SingleSSBOShaderInstance shader, @Nullable Frustum frustum)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ stack.pushPose();
+ this.translateClouds(stack, 0.0D, 0.0D, 0.0D);
+
+ RenderSystem.setShader(() -> shader);
+ prepareShader(shader, stack.last().pose(), shadowMap.getProjMatrix(), this.fogStart, this.fogEnd);
+ shader.apply();
+
+ shadowMap.bind();
+ shadowMap.clear(Minecraft.ON_OSX);
+
+ this.meshGenerator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, opaqueBuffers) ->
+ {
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), opaqueBuffers.getBufferId());
+ this.meshGenerator.getSideMesh().drawInstanced(opaqueBuffers.getElementCount());
+ });
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), 0);
+ GL30.glBindVertexArray(0);
+
+ shadowMap.unbind();
+
+ shader.clear();
+
+ stack.popPose();
+ }
+
+ private float determineShadowMapAngle(float partialTick)
+ {
+ float timeOfDay = this.mc.level.getTimeOfDay(partialTick);
+ return 45.0F * Mth.sin(2.0F * (float)Math.PI * timeOfDay);
+ }
+
+ private void renderShadowMaps(double camX, double camY, double camZ, float partialTick)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ BufferUploader.reset();
+
+ RenderSystem.disableBlend();
+ RenderSystem.enableDepthTest();
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ RenderSystem.disableCull();
+
+ this.stormFogShadowMapStack = this.createShadowMapStack(this.stormFogShadowMap, camX, camY, camZ, s ->
+ {
+ Vector2f direction = this.cloudManager.calculateWindDirection();
+ float yaw = (float)Mth.atan2((double)direction.x, (double)direction.y);
+ s.mulPose(Axis.XP.rotationDegrees(SimpleCloudsConfig.CLIENT.stormFogAngle.get().floatValue()));
+ s.mulPose(Axis.YP.rotation(yaw));
+ });
+ this.renderShadowMap(this.stormFogShadowMap, this.stormFogShadowMapStack, SimpleCloudsShaders.getStormFogShadowMapShader(), this.cullFrustum);
+
+ this.shadowMapStack = this.shadowMap.map(buffer ->
+ {
+ PoseStack stack = this.createShadowMapStack(buffer, camX, camY, camZ, s -> {
+ s.mulPose(Axis.XP.rotationDegrees(90.0F));
+ s.mulPose(Axis.ZN.rotationDegrees(this.determineShadowMapAngle(partialTick)));
+ });
+ this.renderShadowMap(buffer, stack, SimpleCloudsShaders.getCloudsShadowMapShader(), null);
+ return stack;
+ }).orElse(null);
+
+ RenderSystem.enableCull();
+
+ this.mc.getMainRenderTarget().bindWrite(true);
+ }
+
+ public static void renderCloudsDebug(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float partialTick, float fogStart, float fogEnd, @Nullable Frustum frustum, boolean chunkBoundaries, boolean noiseBoundaries)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ if (!generator.canRender())
+ return;
+
+ BufferUploader.reset();
+
+ RenderSystem.disableBlend();
+ RenderSystem.enableDepthTest();
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ RenderSystem.disableCull();
+
+ Tesselator tesselator = Tesselator.getInstance();
+ BufferBuilder builder = tesselator.getBuilder();
+ builder.begin(VertexFormat.Mode.LINES, DefaultVertexFormat.POSITION_COLOR_NORMAL);
+
+ generator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, bufferSet) ->
+ {
+ PreparedChunk preparedChunk = chunk.getChunkInfo();
+ if (chunkBoundaries)
+ {
+ int color = Color.HSBtoRGB((float)preparedChunk.lodLevel() / ((float)generator.getLodConfig().getLods().length + 1), 1.0F, 1.0F);
+ float r = (float)FastColor.ARGB32.red(color) / 255.0F;
+ float g = (float)FastColor.ARGB32.green(color) / 255.0F;
+ float b = (float)FastColor.ARGB32.blue(color) / 255.0F;
+ LevelRenderer.renderLineBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getBoundsMinY() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getBoundsMaxY() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, r, g, b, 1.0F);
+ }
+ if (noiseBoundaries)
+ LevelRenderer.renderLineBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getMinHeight() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getMaxHeight() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, 1.0F, 1.0F, 0.0F, 1.0F);
+ });
+
+ RenderSystem.setShader(GameRenderer::getRendertypeLinesShader);
+ ShaderInstance shader = RenderSystem.getShader();
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+ shader.LINE_WIDTH.set(2.5F);
+ shader.FOG_START.set(Float.MAX_VALUE);
+ shader.apply();
+ BufferUploader.draw(builder.end());
+ shader.clear();
+
+ RenderSystem.enableCull();
+
+ RenderSystem.defaultBlendFunc();
+ RenderSystem.enableBlend();
+
+ builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
+
+ generator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, bufferSet) ->
+ {
+ PreparedChunk preparedChunk = chunk.getChunkInfo();
+ if (chunkBoundaries)
+ {
+ int color = Color.HSBtoRGB((float)preparedChunk.lodLevel() / ((float)generator.getLodConfig().getLods().length + 1), 1.0F, 1.0F);
+ float r = (float)FastColor.ARGB32.red(color) / 255.0F;
+ float g = (float)FastColor.ARGB32.green(color) / 255.0F;
+ float b = (float)FastColor.ARGB32.blue(color) / 255.0F;
+ renderChunkBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getBoundsMinY() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getBoundsMaxY() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, r, g, b, 0.4F);
+ }
+ if (noiseBoundaries)
+ renderChunkBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getMinHeight() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getMaxHeight() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, 1.0F, 1.0F, 0.0F, 0.4F);
+ });
+
+ RenderSystem.setShader(GameRenderer::getPositionColorShader);
+ shader = RenderSystem.getShader();
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+ shader.apply();
+ BufferUploader.draw(builder.end());
+ shader.clear();
+
+ RenderSystem.disableBlend();
+ }
+
+ public float[] getCloudColor(float partialTick)
+ {
+ Vec3 cloudCol = this.mc.level.getCloudColor(partialTick);
+ float factor = this.worldEffectsManager.getDarkenFactor(partialTick, 0.8F);
+ float skyFlashFactor = Math.max(0.0F, ((float)this.mc.level.getSkyFlashTime() - partialTick) * SimpleCloudsConstants.LIGHTNING_FLASH_STRENGTH);
+ factor += skyFlashFactor;
+ float r = Mth.clamp((float)cloudCol.x * factor, 0.0F, 1.0F);
+ float g = Mth.clamp((float)cloudCol.y * factor, 0.0F, 1.0F);
+ float b = Mth.clamp((float)cloudCol.z * factor, 0.0F, 1.0F);
+ return new float[] { r, g, b };
+ }
+
+ public void translateClouds(PoseStack stack, double camX, double camY, double camZ)
+ {
+ stack.translate(-camX, -camY + (double)this.cloudManager.getCloudHeight(), -camZ);
+ stack.scale((float)SimpleCloudsConstants.CLOUD_SCALE, (float)SimpleCloudsConstants.CLOUD_SCALE, (float)SimpleCloudsConstants.CLOUD_SCALE);
+ }
+
+ public void renderWeather(LightTexture texture, float partialTick, double camX, double camY, double camZ)
+ {
+ if (SimpleCloudsCompatHelper.renderCustomRain())
+ this.worldEffectsManager.renderRain(texture, partialTick, camX, camY, camZ);
+ if (!SimpleCloudsMod.dhLoaded())
+ this.worldEffectsManager.renderLightning(partialTick, camX, camY, camZ);
+ }
+
+ public void renderBeforeLevel(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ CloudsRenderPipeline pipeline = CompatHelper.areShadersRunning() ? CloudsRenderPipeline.SHADER_SUPPORT : CloudsRenderPipeline.DEFAULT;
+ DetermineCloudRenderPipelineEvent pipelineEvent = new DetermineCloudRenderPipelineEvent(pipeline);
+ MinecraftForge.EVENT_BUS.post(pipelineEvent);
+ this.renderPipelineThisPass = pipeline;
+ if (pipelineEvent.getOverridenPipeline() != null)
+ this.renderPipelineThisPass = pipelineEvent.getOverridenPipeline();
+
+ float factor = this.worldEffectsManager.getDarkenFactor(partialTick);
+ float renderDistance = (float)this.meshGenerator.getCloudAreaMaxRadius() * (float)SimpleCloudsConstants.CLOUD_SCALE * factor;
+ if (renderDistance < 2867.0F)
+ renderDistance = 2867.0F;
+ ModifyCloudRenderDistanceEvent renderDistEvent = new ModifyCloudRenderDistanceEvent(renderDistance);
+ MinecraftForge.EVENT_BUS.post(renderDistEvent);
+ renderDistance = renderDistEvent.getRenderDistance();
+ this.fogStart = renderDistance / 4.0F;
+ this.fogEnd = renderDistance;
+
+ Entity cameraEntity = this.mc.gameRenderer.getMainCamera().getEntity();
+ if (cameraEntity instanceof LivingEntity living)
+ {
+ var map = living.getActiveEffectsMap();
+ if (map.containsKey(MobEffects.BLINDNESS))
+ {
+ MobEffectInstance instance = map.get(MobEffects.BLINDNESS);
+ float effectFactor = instance.isInfiniteDuration() ? 5.0F : Mth.lerp(Math.min(1.0F, (float)instance.getDuration() / 20.0F), renderDistance, 5.0F);
+ this.fogStart = 0.0F;
+ this.fogEnd = effectFactor * 0.8F;
+ }
+ else if (map.containsKey(MobEffects.DARKNESS))
+ {
+ MobEffectInstance instance = map.get(MobEffects.DARKNESS);
+ if (instance.getFactorData().isPresent())
+ {
+ float f = Mth.lerp(instance.getFactorData().get().getFactor(living, partialTick), renderDistance, 15.0F);
+ this.fogStart = 0.0F;
+ this.fogEnd = f;
+ }
+ }
+ }
+
+ this.meshGenerator.setCullDistance(this.fogEnd / (float)SimpleCloudsConstants.CLOUD_SCALE);
+
+ this.mc.getProfiler().push("simple_clouds_prepare");
+
+ this.cullFrustum = new Frustum(stack.last().pose(), projMat);
+ float scale = (float)SimpleCloudsConstants.CLOUD_SCALE;
+ double originX = camX / scale;
+ double originY = (camY - (double)this.cloudManager.getCloudHeight()) / scale;
+ double originZ = camZ / scale;
+ this.cullFrustum.prepare(originX, originY, originZ);
+
+ ProfilerFiller p = this.mc.getProfiler();
+
+ if (SimpleCloudsConfig.CLIENT.generateMesh.get() && SimpleCloudsCompatHelper.isPrimaryPass())
+ {
+ p.push("mesh_generation");
+ this.prepareMeshGenerator(partialTick);
+ this.meshGenerator.genTick(originX, originY, originZ, SimpleCloudsConfig.CLIENT.frustumCulling.get() ? this.cullFrustum : null, partialTick);
+ p.pop();
+ }
+
+ if (SimpleCloudsConfig.CLIENT.renderClouds.get() && SimpleCloudsCompatHelper.isPrimaryPass())
+ {
+ p.push("shadow_map");
+ this.renderShadowMaps(camX, camY, camZ, partialTick);
+ this.getRenderPipeline().prepare(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ p.pop();
+ }
+
+ this.mc.getProfiler().pop();
+ }
+
+ public void renderAfterSky(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ this.mc.getProfiler().push("simple_clouds_after_sky");
+ this.getRenderPipeline().afterSky(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ this.mc.getProfiler().pop();
+ }
+
+ public void renderBeforeWeather(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ this.mc.getProfiler().push("simple_clouds_before_weather");
+ this.getRenderPipeline().beforeWeather(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ this.mc.getProfiler().pop();
+ }
+
+ public void renderAfterLevel(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ this.mc.getProfiler().push("simple_clouds");
+ this.getRenderPipeline().afterLevel(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ this.mc.getProfiler().pop();
+
+ this.mc.getProfiler().push("world_effects");
+ this.worldEffectsManager.renderPost(stack, partialTick, camX, camY, camZ, (float)SimpleCloudsConstants.CLOUD_SCALE);
+ this.mc.getProfiler().pop();
+ }
+
+ public void doBlurPostProcessing(float partialTick)
+ {
+ if (this.blurPostProcessing != null)
+ {
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.disableBlend();
+ RenderSystem.depthMask(false);
+ this.blurPostProcessing.process(partialTick);
+ RenderSystem.depthMask(true);
+ }
+ }
+
+ public void doScreenSpaceWorldFog(PoseStack stack, Matrix4f projMat, float partialTick)
+ {
+ if (this.screenSpaceWorldFog != null)
+ {
+ RenderSystem.disableBlend();
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ Matrix4f invertedProjMat = new Matrix4f(projMat).invert();
+ Matrix4f invertedModelViewMat = new Matrix4f(stack.last().pose()).invert();
+ for (PostPass pass : ((MixinPostChain)this.screenSpaceWorldFog).simpleclouds$getPostPasses())
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.safeGetUniform("InverseWorldProjMat").set(invertedProjMat);
+ effect.safeGetUniform("InverseModelViewMat").set(invertedModelViewMat);
+ effect.safeGetUniform("FogStart").set(RenderSystem.getShaderFogStart());
+ effect.safeGetUniform("FogEnd").set(RenderSystem.getShaderFogEnd());
+ float[] fogCol = RenderSystem.getShaderFogColor();
+ effect.safeGetUniform("FogColor").set(fogCol[0], fogCol[1], fogCol[2]);
+ effect.safeGetUniform("FogShape").set(RenderSystem.getShaderFogShape().getIndex());
+ }
+
+ this.screenSpaceWorldFog.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+ }
+
+ public void doFinalCompositePass(PoseStack stack, float partialTick, Matrix4f projMat)
+ {
+ if (this.finalComposite != null)
+ {
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ this.finalComposite.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+ }
+
+ public void doStormPostProcessing(PoseStack stack, float partialTick, Matrix4f projMat, double camX, double camY, double camZ, float r, float g, float b)
+ {
+ if (this.stormPostProcessing == null || this.stormFogShadowMapStack == null || this.stormFogShadowMapStack == null)
+ return;
+
+ RenderSystem.disableBlend();
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ this.stormFogTarget.clear(Minecraft.ON_OSX);
+ this.stormFogTarget.bindWrite(true);
+
+ MutableInt size = new MutableInt();
+ boolean flag = SimpleCloudsConfig.CLIENT.stormFogLightningFlashes.get();
+ if (flag)
+ {
+ List lightningBolts = this.worldEffectsManager.getLightningBolts();
+ size.setValue(Math.min(lightningBolts.size(), MAX_LIGHTNING_BOLTS));
+ if (size.getValue() > 0)
+ {
+ this.lightningBoltPositions.writeData(buffer ->
+ {
+ for (int i = 0; i < size.getValue(); i++)
+ {
+ LightningBolt bolt = lightningBolts.get(i);
+ Vector3f pos = bolt.getPosition();
+ buffer.putFloat(pos.x);
+ buffer.putFloat(pos.y);
+ buffer.putFloat(pos.z);
+ buffer.putFloat(bolt.getFade(partialTick));
+ }
+ buffer.rewind();
+ }, size.getValue() * BYTES_PER_LIGHTNING_BOLT, false);
+ }
+ }
+
+ Matrix4f invertedProjMat = new Matrix4f(projMat).invert();
+ Matrix4f invertedModelViewMat = new Matrix4f(stack.last().pose()).invert();
+ for (PostPass pass : ((MixinPostChain)this.stormPostProcessing).simpleclouds$getPostPasses())
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.safeGetUniform("InverseWorldProjMat").set(invertedProjMat);
+ effect.safeGetUniform("InverseModelViewMat").set(invertedModelViewMat);
+ effect.safeGetUniform("ShadowProjMat").set(this.stormFogShadowMap.getProjMatrix());
+ effect.safeGetUniform("ShadowModelViewMat").set(this.stormFogShadowMapStack.last().pose());
+ effect.safeGetUniform("CameraPos").set((float)camX, (float)camY, (float)camZ);
+ effect.safeGetUniform("FogStart").set(this.fogEnd / 2.0F);
+ effect.safeGetUniform("FogEnd").set(this.fogEnd);
+ effect.safeGetUniform("ColorModulator").set(r, g, b, 1.0F);
+ float factor = this.worldEffectsManager.getDarkenFactor(partialTick);
+ effect.safeGetUniform("CutoffDistance").set(1000.0F * factor);
+ effect.safeGetUniform("TotalLightningBolts").set(size.getValue());
+ }
+
+ this.stormPostProcessing.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+
+ public void doCloudShadowProcessing(PoseStack stack, float partialTick, Matrix4f projMat, double camX, double camY, double camZ, int depthBufferId)
+ {
+ if (this.cloudShadows == null || this.shadowMap.isEmpty() || this.shadowMapStack == null)
+ return;
+
+ RenderSystem.disableBlend();
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ Matrix4f invertedProjMat = new Matrix4f(projMat).invert();
+ Matrix4f invertedModelViewMat = new Matrix4f(stack.last().pose()).invert();
+ float minimumRadius = this.mc.gameRenderer.getRenderDistance();
+ for (PostPass pass : ((MixinPostChain)this.cloudShadows).simpleclouds$getPostPasses())
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("DepthSampler", () -> depthBufferId);
+ effect.safeGetUniform("InverseWorldProjMat").set(invertedProjMat);
+ effect.safeGetUniform("InverseModelViewMat").set(invertedModelViewMat);
+ effect.safeGetUniform("ShadowProjMat").set(this.shadowMap.get().getProjMatrix());
+ effect.safeGetUniform("ShadowModelViewMat").set(this.shadowMapStack.last().pose());
+ effect.safeGetUniform("CameraPos").set((float)camX, (float)camY, (float)camZ);
+ effect.safeGetUniform("MinimumRadius").set(minimumRadius);
+ }
+
+ this.cloudShadows.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+
+ public static void prepareShader(ShaderInstance shader, Matrix4f modelView, Matrix4f projMat, float fogStart, float fogEnd)
+ {
+ for (int i = 0; i < 12; ++i)
+ {
+ int j = RenderSystem.getShaderTexture(i);
+ shader.setSampler("Sampler" + i, j);
+ }
+
+ if (shader.MODEL_VIEW_MATRIX != null)
+ shader.MODEL_VIEW_MATRIX.set(modelView);
+
+ if (shader.PROJECTION_MATRIX != null)
+ shader.PROJECTION_MATRIX.set(projMat);
+
+ if (shader.INVERSE_VIEW_ROTATION_MATRIX != null)
+ shader.INVERSE_VIEW_ROTATION_MATRIX.set(RenderSystem.getInverseViewRotationMatrix());
+
+ if (shader.COLOR_MODULATOR != null)
+ shader.COLOR_MODULATOR.set(RenderSystem.getShaderColor());
+
+ if (shader.GLINT_ALPHA != null)
+ shader.GLINT_ALPHA.set(RenderSystem.getShaderGlintAlpha());
+
+ if (shader.FOG_START != null)
+ shader.FOG_START.set(fogStart);
+
+ if (shader.FOG_END != null)
+ shader.FOG_END.set(fogEnd);
+
+ if (shader.FOG_COLOR != null)
+ shader.FOG_COLOR.set(RenderSystem.getShaderFogColor());
+
+ if (shader.FOG_SHAPE != null)
+ shader.FOG_SHAPE.set(RenderSystem.getShaderFogShape().getIndex());
+
+ if (shader.TEXTURE_MATRIX != null)
+ shader.TEXTURE_MATRIX.set(RenderSystem.getTextureMatrix());
+
+ if (shader.GAME_TIME != null)
+ shader.GAME_TIME.set(RenderSystem.getShaderGameTime());
+
+ if (shader.SCREEN_SIZE != null)
+ {
+ Window window = Minecraft.getInstance().getWindow();
+ shader.SCREEN_SIZE.set((float) window.getWidth(), (float) window.getHeight());
+ }
+
+ shader.safeGetUniform("UseNormals").set(SimpleCloudsConfig.CLIENT.cubeNormals.get() ? 1 : 0);
+
+ RenderSystem.setShaderLights(DIFFUSE_LIGHT_0, DIFFUSE_LIGHT_1);
+ RenderSystem.setupShaderLights(shader);
+ }
+
+ public void copyDepthFromCloudsToMain()
+ {
+ this._copyDepthSafe(this.mc.getMainRenderTarget(), this.cloudTarget);
+ }
+
+ public void copyDepthFromMainToClouds()
+ {
+ this._copyDepthSafe(this.cloudTarget, this.mc.getMainRenderTarget());
+ }
+
+ public void copyDepthFromCloudsToTransparency()
+ {
+ this._copyDepthSafe(this.cloudTransparencyTarget, this.cloudTarget);
+ }
+
+ private void _copyDepthSafe(RenderTarget to, RenderTarget from)
+ {
+ RenderSystem.assertOnRenderThread();
+ GlStateManager._getError(); //Clear old error
+ if (!this.failedToCopyDepthBuffer)
+ {
+ to.bindWrite(false);
+ to.copyDepthFrom(from);
+ if (GlStateManager._getError() != GL11.GL_INVALID_OPERATION)
+ return;
+ boolean enabledStencil = false;
+ if (to.isStencilEnabled() && !from.isStencilEnabled())
+ {
+ from.enableStencil();
+ enabledStencil = true;
+ }
+ else if (from.isStencilEnabled() && !to.isStencilEnabled())
+ {
+ to.enableStencil();
+ enabledStencil = true;
+ }
+ if (enabledStencil)
+ {
+ to.copyDepthFrom(from);
+ if (GlStateManager._getError() == GL11.GL_INVALID_OPERATION)
+ {
+ LOGGER.error("Unable to copy depth between the main and clouds frame buffers, even after enabling stencil. Please note that the clouds may not render properly.");
+ this.failedToCopyDepthBuffer = true;
+ }
+ else
+ {
+ LOGGER.info("NOTE: Please ignore the above OpenGL error. Simple Clouds had to toggle stencil in order to copy the depth buffer between the main and clouds frame buffers.");
+ }
+ }
+ else
+ {
+ LOGGER.error("Unable to copy depth between the main and clouds frame buffers. Please note that the clouds may not render properly.");
+ this.failedToCopyDepthBuffer = true;
+ }
+ }
+ }
+
+ public void fillReport(CrashReport report)
+ {
+ CrashReportCategory category = report.addCategory("Simple Clouds Renderer");
+ category.setDetail("Cloud Mode", this.settings.getCurrentCloudMode());
+ category.setDetail("Cloud Target Available", this.cloudTarget != null);
+ category.setDetail("Storm Fog Target Active", this.stormFogTarget != null);
+ category.setDetail("Blur Target Active", this.blurTarget != null);
+ category.setDetail("Transparency Target Active", this.cloudTransparencyTarget != null);
+ category.setDetail("Post Chains", this.postChains.toString());
+ category.setDetail("Lightning Bolt SSBO", this.lightningBoltPositions);
+ category.setDetail("Clouds Shadow Map", this.stormFogShadowMap);
+ category.setDetail("Storm Fog Shadow Map", this.stormFogShadowMap);
+ category.setDetail("Failed to copy depth buffer", this.failedToCopyDepthBuffer);
+ category.setDetail("Needs Reload", this.needsReload);
+
+ CrashReportCategory meshGenCategory = report.addCategory("Cloud Mesh Generator");
+ if (this.meshGenerator != null)
+ {
+ meshGenCategory.setDetail("Type", this.meshGenerator.toString());
+ this.meshGenerator.fillReport(meshGenCategory);
+ }
+ else
+ {
+ meshGenCategory.setDetail("Type", "Mesh generator is not initialized");
+ }
+ }
+
+ public static void initialize(CloudsRendererSettings settings)
+ {
+ RenderSystem.assertOnRenderThread();
+ if (instance != null)
+ throw new IllegalStateException("Simple Clouds renderer is already initialized");
+ instance = new SimpleCloudsRenderer(settings, Minecraft.getInstance());
+ LOGGER.debug("Clouds render initialized");
+ }
+
+ public static SimpleCloudsRenderer getInstance()
+ {
+ return Objects.requireNonNull(instance, "Renderer not initialized!");
+ }
+
+ public static Optional getOptionalInstance()
+ {
+ return Optional.ofNullable(instance);
+ }
+
+ public static boolean canRenderInDimension(@Nullable ClientLevel level)
+ {
+ if (level == null)
+ return false;
+
+ List extends String> whitelist;
+ boolean useAsBlacklist;
+ if (ClientCloudManager.isAvailableServerSide() && SimpleCloudsConfig.SERVER_SPEC.isLoaded())
+ {
+ whitelist = SimpleCloudsConfig.SERVER.dimensionWhitelist.get();
+ useAsBlacklist = SimpleCloudsConfig.SERVER.whitelistAsBlacklist.get();
+ }
+ else
+ {
+ whitelist = SimpleCloudsConfig.CLIENT.dimensionWhitelist.get();
+ useAsBlacklist = SimpleCloudsConfig.CLIENT.whitelistAsBlacklist.get();
+ }
+
+ boolean flag = whitelist.stream().anyMatch(val -> {
+ return level.dimension().location().toString().equals(val);
+ });
+
+ return useAsBlacklist ? !flag : flag;
+ }
+
+ private static void renderChunkBox(VertexConsumer consumer, float minX, float minY, float minZ, float maxX, float maxY, float maxZ, float r, float g, float b, float a)
+ {
+ //-X
+ consumer.vertex(minX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, minZ).color(r, g, b, a).endVertex();
+
+ //+X
+ consumer.vertex(maxX, minY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, minY, maxZ).color(r, g, b, a).endVertex();
+
+ //-Y
+ consumer.vertex(maxX, minY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, minZ).color(r, g, b, a).endVertex();
+
+ //+Y
+ consumer.vertex(minX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, minZ).color(r, g, b, a).endVertex();
+
+ //-Z
+ consumer.vertex(minX, minY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, minY, minZ).color(r, g, b, a).endVertex();
+
+ //+Z
+ consumer.vertex(maxX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, maxZ).color(r, g, b, a).endVertex();
+ }
+
+ private static int calculateMeshGenInterval()
+ {
+ int fps = Minecraft.getInstance().getFps();
+ switch (SimpleCloudsConfig.CLIENT.generationInterval.get())
+ {
+ case STATIC:
+ {
+ return SimpleCloudsConfig.CLIENT.framesToGenerateMesh.get();
+ }
+ case DYNAMIC:
+ {
+ return Math.max(Mth.ceil((130.0F - (float)fps) / 30.0F) + 5, 1);
+ }
+ case TARGET_FPS:
+ {
+ return Math.max(Mth.ceil((float)fps / SimpleCloudsConfig.CLIENT.targetMeshGenFps.get()), 1);
+ }
+ default:
+ return 5;
+ }
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/client/renderer/pipeline/DefaultPipeline.java b/dev/nonamecrackers2/simpleclouds/client/renderer/pipeline/DefaultPipeline.java
new file mode 100644
index 00000000..9c89df20
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/client/renderer/pipeline/DefaultPipeline.java
@@ -0,0 +1,155 @@
+package dev.nonamecrackers2.simpleclouds.client.renderer.pipeline;
+
+import org.joml.Matrix4f;
+
+import com.mojang.blaze3d.pipeline.RenderTarget;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexSorting;
+
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.FrameBufferUtils;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.WeightedBlendingTarget;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.CloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.renderer.SimpleCloudsRenderer;
+import dev.nonamecrackers2.simpleclouds.client.world.FogRenderMode;
+import dev.nonamecrackers2.simpleclouds.common.config.SimpleCloudsConfig;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.util.profiling.ProfilerFiller;
+import net.minecraft.world.level.material.FogType;
+import nonamecrackers2.crackerslib.common.compat.CompatHelper;
+
+public class DefaultPipeline implements CloudsRenderPipeline
+{
+ protected DefaultPipeline() {}
+
+ @Override
+ public void prepare(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum) {}
+
+ @Override
+ public void afterSky(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum)
+ {
+ ProfilerFiller p = mc.getProfiler();
+
+ float[] cloudCol = renderer.getCloudColor(partialTick);
+ float cloudR = (float)cloudCol[0];
+ float cloudG = (float)cloudCol[1];
+ float cloudB = (float)cloudCol[2];
+
+ if (SimpleCloudsConfig.CLIENT.atmosphericClouds.get())
+ {
+ p.push("atmospheric_clouds");
+ renderer.getAtmosphericCloudRenderer().render(stack, projMat, partialTick, camX, camY, camZ, cloudR, cloudG, cloudB);
+ mc.getMainRenderTarget().bindWrite(false);
+ p.pop();
+ }
+
+ // Clouds
+
+ p.push("clouds");
+
+ stack.pushPose();
+
+ renderer.translateClouds(stack, camX, camY, camZ); // Prepare render for origin of camera
+
+ // Render opaque cloud geometry
+ p.push("clouds_opaque");
+
+ // Clears the cloud framebuffer and sets it as the current one
+ RenderTarget cloudTarget = renderer.getCloudTarget();
+ cloudTarget.clear(Minecraft.ON_OSX);
+ cloudTarget.bindWrite(false);
+
+ // Renders the clouds on to the cloud frame buffer
+ CloudMeshGenerator generator = renderer.getMeshGenerator();
+ SimpleCloudsRenderer.renderCloudsOpaque(generator, stack, projMat, renderer.getFogStart(), renderer.getFogEnd(), partialTick, cloudR, cloudG, cloudB, SimpleCloudsConfig.CLIENT.frustumCulling.get() ? frustum : null);
+
+ // Here we copy the depth from the cloud frame buffer to the main one, so we can have correct depth information with the
+ // rest of the Minecraft world
+ renderer.copyDepthFromCloudsToMain();
+
+ // Render transparent cloud geometry
+ p.popPush("clouds_transparent");
+
+ WeightedBlendingTarget transparencyTarget = renderer.getCloudTransparencyTarget();
+ transparencyTarget.clear(Minecraft.ON_OSX);
+
+ if (generator.transparencyEnabled())
+ {
+ // We use weighted order independent transparency as we cannot easily sort the cloud mesh
+ // More info here https://jcgt.org/published/0002/02/09/paper.pdf and http://casual-effects.blogspot.com/2015/03/implemented-weighted-blended-order.html
+ renderer.copyDepthFromCloudsToTransparency(); // Copy the depth data from the cloud framebuffer so we don't get weird depth issues
+ transparencyTarget.bindWrite(false);
+
+ // Render the transparent geometry to the transparency framebuffer
+ SimpleCloudsRenderer.renderCloudsTransparency(generator, stack, projMat, renderer.getFogStart(), renderer.getFogEnd(), partialTick, cloudR, cloudG, cloudB, SimpleCloudsConfig.CLIENT.frustumCulling.get() ? frustum : null);
+ }
+
+ p.pop();
+
+ stack.popPose();
+
+ // Render everything on to the main screen using a final composite shader
+ p.push("clouds_composite");
+ renderer.doFinalCompositePass(stack, partialTick, projMat);
+ p.pop();
+
+ p.pop();
+
+ // Storm Fog
+
+ if (SimpleCloudsConfig.CLIENT.renderStormFog.get())
+ {
+ p.push("storm_fog");
+
+ // Renders the storm fog at a lower resolution
+ renderer.doStormPostProcessing(stack, partialTick, projMat, camX, camY, camZ, cloudR, cloudG, cloudB);
+
+ // Next we blit the storm fog to a higher resolution texture and apply a box blur
+ RenderTarget target = renderer.getBlurTarget();
+ target.clear(Minecraft.ON_OSX); // Clear old contents on the blur framebuffer
+ target.bindWrite(true); // Bind write and resize viewport
+ // Here we blit the contents of the storm fog framebuffer on to the blur framebuffer. A special function is used here
+ // to preserve the alpha channel when rendering
+ FrameBufferUtils.blitTargetPreservingAlpha(renderer.getStormFogTarget(), mc.getWindow().getWidth(), mc.getWindow().getHeight());
+ // Blurs the storm fog
+ renderer.doBlurPostProcessing(partialTick);
+ // Renders the storm fog to the screen
+ mc.getMainRenderTarget().bindWrite(false);
+ RenderSystem.enableBlend();
+ RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE);
+ renderer.getBlurTarget().blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight(), false);
+ RenderSystem.disableBlend();
+ RenderSystem.defaultBlendFunc();
+ // Need to do this here because blitToScreen messes up the projection matrix and doesn't set it back
+ RenderSystem.setProjectionMatrix(projMat, VertexSorting.DISTANCE_TO_ORIGIN);
+
+ p.pop();
+ }
+
+ // Set the frame buffer back to the main one so everything else can render normally
+ mc.getMainRenderTarget().bindWrite(CompatHelper.isVrActive());
+ }
+
+ @Override
+ public void beforeWeather(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum)
+ {
+ if (SimpleCloudsConfig.CLIENT.fogMode.get() == FogRenderMode.SCREEN_SPACE && mc.gameRenderer.getMainCamera().getFluidInCamera() == FogType.NONE)
+ {
+ renderer.doScreenSpaceWorldFog(stack, projMat, partialTick);
+ mc.getMainRenderTarget().bindWrite(false);
+ }
+ }
+
+ @Override
+ public void afterLevel(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum)
+ {
+// mc.getProfiler().push("clouds_debug");
+// stack.pushPose();
+// renderer.translateClouds(stack, camX, camY, camZ);
+// SimpleCloudsRenderer.renderCloudsDebug(renderer.getMeshGenerator(), stack, projMat, partialTick, renderer.getFogStart(), renderer.getFogEnd(), frustum, false, true);
+// stack.popPose();
+// mc.getProfiler().pop();
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/client/renderer/pipeline/ShaderSupportPipeline.java b/dev/nonamecrackers2/simpleclouds/client/renderer/pipeline/ShaderSupportPipeline.java
new file mode 100644
index 00000000..5ea1293d
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/client/renderer/pipeline/ShaderSupportPipeline.java
@@ -0,0 +1,121 @@
+package dev.nonamecrackers2.simpleclouds.client.renderer.pipeline;
+
+import org.joml.Matrix4f;
+
+import com.mojang.blaze3d.pipeline.RenderTarget;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexSorting;
+
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.FrameBufferUtils;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.WeightedBlendingTarget;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.CloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.renderer.SimpleCloudsRenderer;
+import dev.nonamecrackers2.simpleclouds.common.config.SimpleCloudsConfig;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.util.profiling.ProfilerFiller;
+import nonamecrackers2.crackerslib.common.compat.CompatHelper;
+
+public class ShaderSupportPipeline implements CloudsRenderPipeline
+{
+ protected ShaderSupportPipeline() {}
+
+ @Override
+ public void prepare(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum) {}
+
+ @Override
+ public void afterSky(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum) {}
+
+ @Override
+ public void beforeWeather(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum) {}
+
+ // To make Iris shaders work at a bare minimum, we render the clouds after the Iris render pipeline
+ @Override
+ public void afterLevel(Minecraft mc, SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ, Frustum frustum)
+ {
+ ProfilerFiller p = mc.getProfiler();
+
+ float[] cloudCol = renderer.getCloudColor(partialTick);
+ float cloudR = (float)cloudCol[0];
+ float cloudG = (float)cloudCol[1];
+ float cloudB = (float)cloudCol[2];
+
+ if (CompatHelper.areShadersRunning())
+ GlStateManager._depthMask(true);
+
+ // Render opaque cloud geometry
+ p.push("clouds_opaque");
+
+ stack.pushPose();
+
+ renderer.translateClouds(stack, camX, camY, camZ);
+
+ RenderTarget cloudTarget = renderer.getCloudTarget();
+ cloudTarget.clear(Minecraft.ON_OSX);
+ renderer.copyDepthFromMainToClouds(); // Copy depth from main framebuffer
+ cloudTarget.bindWrite(false);
+
+ // Renders the clouds on to the cloud frame buffer
+ CloudMeshGenerator generator = renderer.getMeshGenerator();
+ SimpleCloudsRenderer.renderCloudsOpaque(renderer.getMeshGenerator(), stack, projMat, renderer.getFogStart(), renderer.getFogEnd(), partialTick, cloudR, cloudG, cloudB, SimpleCloudsConfig.CLIENT.frustumCulling.get() ? frustum : null);
+
+ // Render transparent cloud geometry
+ p.popPush("clouds_transparent");
+
+ WeightedBlendingTarget transparencyTarget = renderer.getCloudTransparencyTarget();
+ transparencyTarget.clear(Minecraft.ON_OSX);
+
+ if (generator.transparencyEnabled())
+ {
+ // We use weighted order independent transparency as we cannot easily sort the cloud mesh
+ // More info here https://jcgt.org/published/0002/02/09/paper.pdf and http://casual-effects.blogspot.com/2015/03/implemented-weighted-blended-order.html
+ renderer.copyDepthFromCloudsToTransparency(); // Copy the depth data from the cloud framebuffer so we don't get weird depth issues
+ transparencyTarget.bindWrite(false);
+
+ // Render the transparent geometry to the transparency framebuffer
+ SimpleCloudsRenderer.renderCloudsTransparency(generator, stack, projMat, renderer.getFogStart(), renderer.getFogEnd(), partialTick, cloudR, cloudG, cloudB, SimpleCloudsConfig.CLIENT.frustumCulling.get() ? frustum : null);
+ }
+
+ p.pop();
+
+ stack.popPose();
+
+ // Render everything on to the main screen using a final composite shader
+ p.push("clouds_composite");
+ renderer.doFinalCompositePass(stack, partialTick, projMat);
+ p.pop();
+
+ mc.getMainRenderTarget().bindWrite(false);
+
+ if (SimpleCloudsConfig.CLIENT.renderStormFog.get())
+ {
+ p.push("storm_fog");
+
+ // Renders the storm fog at a lower resolution
+ renderer.doStormPostProcessing(stack, partialTick, projMat, camX, camY, camZ, cloudR, cloudG, cloudB);
+
+ // Next we blit the storm fog to a higher resolution texture and apply a box blur
+ RenderTarget target = renderer.getBlurTarget();
+ target.clear(Minecraft.ON_OSX); // Clear old contents on the blur framebuffer
+ target.bindWrite(true); // Bind write and resize viewport
+ // Here we blit the contents of the storm fog framebuffer on to the blur framebuffer. A special function is used here
+ // to preserve the alpha channel when rendering
+ FrameBufferUtils.blitTargetPreservingAlpha(renderer.getStormFogTarget(), mc.getWindow().getWidth(), mc.getWindow().getHeight());
+ // Blurs the storm fog
+ renderer.doBlurPostProcessing(partialTick);
+ // Renders the storm fog to the screen
+ mc.getMainRenderTarget().bindWrite(false);
+ RenderSystem.enableBlend();
+ RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE);
+ renderer.getBlurTarget().blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight(), false);
+ RenderSystem.disableBlend();
+ RenderSystem.defaultBlendFunc();
+ // Need to do this here because blitToScreen messes up the projection matrix and doesn't set it back
+ RenderSystem.setProjectionMatrix(projMat, VertexSorting.DISTANCE_TO_ORIGIN);
+
+ p.pop();
+ }
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/common/cloud/spawning/CloudGenerator.java b/dev/nonamecrackers2/simpleclouds/common/cloud/spawning/CloudGenerator.java
new file mode 100644
index 00000000..a65053c4
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/common/cloud/spawning/CloudGenerator.java
@@ -0,0 +1,413 @@
+package dev.nonamecrackers2.simpleclouds.common.cloud.spawning;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.joml.Vector2f;
+import org.joml.Vector2i;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import dev.nonamecrackers2.simpleclouds.api.SimpleCloudsAPI;
+import dev.nonamecrackers2.simpleclouds.api.common.cloud.spawning.CreateRegionFunction;
+import dev.nonamecrackers2.simpleclouds.api.common.cloud.spawning.SpawnInfo;
+import dev.nonamecrackers2.simpleclouds.api.common.event.CloudRegionNaturallySpawnEvent;
+import dev.nonamecrackers2.simpleclouds.api.common.event.CloudRegionRemovedEvent;
+import dev.nonamecrackers2.simpleclouds.common.api.ScAPICloudGeneratorImplHelper;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudTypeSource;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudRegion;
+import dev.nonamecrackers2.simpleclouds.common.world.SpawnRegion;
+import net.minecraft.util.Mth;
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.Vec2;
+import net.minecraftforge.common.MinecraftForge;
+
+public abstract class CloudGenerator implements ScAPICloudGeneratorImplHelper
+{
+ private static final Logger LOGGER = LogManager.getLogger("simpleclouds/CloudGenerator");
+ private List spawnRegions = Lists.newArrayList();
+ private final List clouds = Lists.newArrayList();
+ protected RandomSource random = RandomSource.create();
+ protected final Supplier spawnConfig;
+ protected int ticksTillNextGen;
+ protected final CloudTypeSource cloudGetter;
+
+ public CloudGenerator(CloudTypeSource cloudGetter, Supplier spawnConfig)
+ {
+ this.cloudGetter = cloudGetter;
+ this.spawnConfig = spawnConfig;
+ }
+
+ public int getTicksTillNextGen()
+ {
+ return this.ticksTillNextGen;
+ }
+
+ public Supplier getSpawnConfig()
+ {
+ return this.spawnConfig;
+ }
+
+ @Override
+ public final List getClouds()
+ {
+ return ImmutableList.copyOf(this.clouds);
+ }
+
+ @Override
+ public final List getSpawnRegions()
+ {
+ return ImmutableList.copyOf(this.spawnRegions);
+ }
+
+ @Override
+ public List getCloudsInRegion(SpawnRegion region)
+ {
+ List clouds = Lists.newArrayList();
+ for (CloudRegion cloud : this.clouds)
+ {
+ if (cloud.intersects(region))
+ clouds.add(cloud);
+ }
+ return clouds;
+ }
+
+ @Override
+ public @Nullable CloudRegion getCloudAtWorldPosition(float worldX, float worldZ)
+ {
+ return this.getCloudAtPosition(worldX / (float)SimpleCloudsConstants.CLOUD_SCALE, worldZ / (float)SimpleCloudsConstants.CLOUD_SCALE);
+ }
+
+ @Override
+ public @Nullable CloudRegion getCloudAtPosition(float x, float z)
+ {
+ return CloudRegion.calculateAt(this.getClouds(), x, z).getLeft();
+ }
+
+ @Override
+ public List getRegionsThatOccupyCloud(CloudRegion cloud)
+ {
+ List regions = Lists.newArrayList();
+ for (SpawnRegion region : this.spawnRegions)
+ {
+ if (cloud.intersects(region))
+ regions.add(region);
+ }
+ return regions;
+ }
+
+ @Override
+ public final int getTotalCloudRegions()
+ {
+ return this.clouds.size();
+ }
+
+ @Override
+ public void setClouds(Collection clouds)
+ {
+ this.removeAllClouds();
+ clouds.forEach(r -> {
+ this.clouds.add(r);
+ });
+ }
+
+ @Override
+ public boolean removeAllClouds()
+ {
+ return this.removeClouds(r -> true);
+ }
+
+ @Override
+ public boolean removeClouds(Predicate predicate)
+ {
+ return this.removeCloudsCount(predicate) > 0;
+ }
+
+ @Override
+ public int removeCloudsCount(Predicate predicate)
+ {
+ int count = 0;
+ var iterator = this.clouds.iterator();
+ while (iterator.hasNext())
+ {
+ CloudRegion region = iterator.next();
+ if (predicate.test(region))
+ {
+ iterator.remove();
+ MinecraftForge.EVENT_BUS.post(new CloudRegionRemovedEvent(null, region, CloudRegionRemovedEvent.Reason.MANUALLY));
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public boolean addCloud(CloudRegion region, CloudGenerator.Order order)
+ {
+ if (!this.cloudGetter.doesCloudTypeExist(region.getCloudTypeId()))
+ {
+ LOGGER.warn("Attempted to spawn a cloud formation: unknown id '{}'", region.getCloudTypeId());
+ return false;
+ }
+
+ if (this.clouds.contains(region))
+ return false;
+
+ // Ensures we wont go over the maximum cloud formations for all regions that would include
+ // this cloud formation
+ for (SpawnRegion spawnRegion : this.getRegionsThatOccupyCloud(region))
+ {
+ int totalCount = 0;
+ for (CloudRegion cloud : this.clouds)
+ {
+ if (cloud.intersects(spawnRegion))
+ totalCount++;
+ }
+ if (totalCount >= SimpleCloudsConstants.MAX_CLOUD_FORMATIONS)
+ {
+// System.out.println("refusing cloud region, too many");
+ return false;
+ }
+ }
+
+ order.appender.accept(this.clouds, region);
+
+// System.out.println(this.clouds.stream().map(CloudRegion::getOrderWeight).toList());
+
+ return true;
+ }
+
+ public void initialize(RandomSource random, Level level)
+ {
+ this.random = RandomSource.create();
+ this.spawnRegions = this.determineValidSpawnRegions(this.random, level);
+ this.removeAllClouds();
+ CloudSpawningConfig config = this.spawnConfig.get();
+ this.ticksTillNextGen = config.getSpawnInterval().sample(this.random);
+ }
+
+ public void tick(@Nullable Level level, float speed)
+ {
+ this.spawnRegions = this.determineValidSpawnRegions(this.random, level);
+
+ var iterator = this.clouds.iterator();
+ while (iterator.hasNext())
+ {
+ CloudRegion region = iterator.next();
+
+ //NOTE: If a cloud formation (region) is on the edge of a spawn region and is not visible, if the player moves even a slightly bit they can make that
+ //formation visible again causing it to tick. It could then move outside the region again, then the player can move and make it become
+ //visible again causing a cycle. This shouldn't happen as often since cloud formations shrink and will shrink extra fast when no longer visible,
+ //making it so they will shrink farther away from the edge of a spawn region preventing this, but it is behavior to note
+ boolean isVisible = SpawnRegion.doesCircleIntersect(this.spawnRegions, region.getWorldX(), region.getWorldZ(), region.getWorldRadius() / region.getStretch() + (float)SimpleCloudsConstants.CLOUD_SCALE / SimpleCloudsConstants.REGION_EDGE_FADE_FACTOR);
+ if (isVisible != region.wasPriorVisible())
+ this.onRegionVisibilityChange(region, isVisible);
+ region.tick(this.random, level, isVisible, speed);
+
+ if (!this.cloudGetter.doesCloudTypeExist(region.getCloudTypeId()))
+ {
+ LOGGER.warn("Cloud type with id {} no longer exists, removing cloud region", region.getCloudTypeId());
+ iterator.remove();
+ MinecraftForge.EVENT_BUS.post(new CloudRegionRemovedEvent(level, region, CloudRegionRemovedEvent.Reason.CLOUD_TYPE_NO_LONGER_EXISTS));
+ }
+
+ if (region.isDead())
+ {
+ iterator.remove();
+ CloudRegionRemovedEvent.Reason reason = CloudRegionRemovedEvent.Reason.NATURALLY;
+ if (!region.wasPriorVisible())
+ reason = CloudRegionRemovedEvent.Reason.NO_LONGER_VISIBLE;
+ MinecraftForge.EVENT_BUS.post(new CloudRegionRemovedEvent(level, region, reason));
+// if (level != null && !level.isClientSide)
+// System.out.println("cloud region died, was visible: " + isVisible + ", total: " + this.getTotalCloudRegions());
+ }
+ }
+
+ if (this.ticksTillNextGen > 0)
+ this.ticksTillNextGen -= Math.max(1, Mth.ceil(speed));
+
+ CloudSpawningConfig config = this.spawnConfig.get();
+
+ // In case the spawning config changes and the spawn interval is very different
+ int maxSpawnInterval = config.getSpawnInterval().getMaxValue();
+ if (this.ticksTillNextGen > maxSpawnInterval)
+ this.ticksTillNextGen = maxSpawnInterval;
+
+ if (!SimpleCloudsAPI.getApi().getHooks().isExternalWeatherControlEnabled())
+ {
+ if (!config.isEmpty() && this.shouldGenerateCloud(config, this.random, level))
+ this.spawnCloud(config, level);
+ }
+ }
+
+ protected boolean shouldGenerateCloud(CloudSpawningConfig config, RandomSource random, Level level)
+ {
+ return this.ticksTillNextGen <= 0;
+ }
+
+ public Optional spawnCloud(CloudSpawningConfig config, Level level)
+ {
+ return this.spawnCloud(() -> config.getRandom(this.random).orElse(null), config.getSpawnInterval().sample(this.random), config.getMaxRegions(), level);
+ }
+
+ @Override
+ public Optional spawnCloud(Supplier infoGetter, int nextSpawnInterval, int maxRegions, Level level)
+ {
+ return this.spawnCloud(infoGetter, nextSpawnInterval, maxRegions, level, this::createRegion);
+ }
+
+ @Override
+ public Optional spawnCloud(Supplier infoGetter, int nextSpawnInterval, int maxRegions, Level level, CreateRegionFunction regionFunc)
+ {
+ this.ticksTillNextGen = nextSpawnInterval;
+// System.out.println("next spawn attempt: " + this.ticksTillNextGen);
+
+ MutableObject spawnedCloud = new MutableObject<>();
+
+ SpawnRegion.randomPointForEachRegion(this.spawnRegions, this.random, SimpleCloudsConstants.SPAWN_ATTEMPTS, (r, p) ->
+ {
+ if (this.getCloudsInRegion(r).size() >= maxRegions)
+ return true;
+
+ float x = (float)p.x + 0.5F;
+ float z = (float)p.y + 0.5F;
+
+ SpawnInfo info = infoGetter.get();
+
+ if (info == null)
+ return false;
+
+ CloudType type = this.cloudGetter.getCloudTypeForId(info.cloudType());
+ if (type == null)
+ {
+ LOGGER.warn("Spawn config has unknown cloud type with id '{}'", info.cloudType());
+ return false;
+ }
+
+ return regionFunc.create(infoGetter.get(), (float)r.x() + 0.5F, (float)r.z() + 0.5F, x, z, this.random, true).map(apiRegion ->
+ {
+ CloudRegion region = (CloudRegion)apiRegion;
+ if (this.addCloud(region, CloudGenerator.Order.USE_WEIGHT))
+ {
+ spawnedCloud.setValue(region);
+ MinecraftForge.EVENT_BUS.post(new CloudRegionNaturallySpawnEvent(level, apiRegion));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }).orElse(false);
+ });
+
+ return Optional.ofNullable(spawnedCloud.getValue());
+ }
+
+ @Override
+ public Optional createRegion(SpawnInfo info, float playerX, float playerZ, float x, float z, RandomSource random, boolean growTime)
+ {
+ for (CloudRegion region : this.getClouds())
+ {
+ float dist = Vector2f.distance(x, z, region.getWorldX(), region.getWorldZ()) - region.getWorldRadius();
+ if (dist <= SimpleCloudsConstants.MIN_SPAWN_DIST_BETWEEN_REGIONS)
+ return Optional.empty();
+ }
+
+ float deltaAdj = info.movesToPlayer() ? 0.1F : 1.0F;
+ float deltaX = (playerX - x) * (1.0F + random.nextFloat() * deltaAdj);
+ float deltaZ = (playerZ - z) * (1.0F + random.nextFloat() * deltaAdj);
+ float rotation = (float)Math.atan2(deltaX, deltaZ) + (float)Math.PI;
+ Vec2 direction;
+ if (random.nextInt(5) == 0)
+ direction = new Vec2(random.nextFloat() * 2.0F - 1.0F, random.nextFloat() * 2.0F - 1.0F).normalized();
+ else
+ direction = new Vec2(deltaX, deltaZ).normalized();
+
+ float radius = (float)info.determineRadius(random);
+ float maxSpeed = info.determineSpeed(random);
+ float accelerationFactor = 0.01F;
+ int existTicks = info.determineExistTicks(random);
+ int growTicks = growTime ? info.determineGrowTicks(random) : 0;
+ float stretchFactor = info.determineStretchFactor(random);
+
+ return Optional.of(new CloudRegion(info.cloudType(), direction, maxSpeed, accelerationFactor, x / (float)SimpleCloudsConstants.CLOUD_SCALE, z / (float)SimpleCloudsConstants.CLOUD_SCALE, radius / (float)SimpleCloudsConstants.CLOUD_SCALE, rotation, stretchFactor, existTicks, growTicks, info.orderWeight()));
+ }
+
+ public void doInitialGen(int x, int z, Level level, boolean ignoreOtherRegions)
+ {
+ SpawnRegion region = new SpawnRegion(x, z, SimpleCloudsConstants.SPAWN_RADIUS);
+
+ CloudSpawningConfig config = this.spawnConfig.get();
+
+ if (this.getCloudsInRegion(region).size() > config.getMaxInitialRegions())
+ return;
+
+ for (int i = 0; i < config.getMaxInitialRegions(); i++)
+ {
+ for (int j = 0; j < SimpleCloudsConstants.SPAWN_ATTEMPTS; j++)
+ {
+ Vector2i pos = SpawnRegion.getRandomPointInRegion(region, this.random);
+ if (this.getCloudsInRegion(region).size() >= config.getMaxInitialRegions())
+ continue;
+ if (!ignoreOtherRegions && this.spawnRegions.stream().anyMatch(r -> r.includesPoint(pos.x, pos.y)))
+ continue;
+ CloudRegion cloudFormation = this.createRegion(config.getRandom(this.random).orElse(null), (float)x + 0.5F, (float)z + 0.5F, (float)pos.x + 0.5F, (float)pos.y + 0.5F, this.random, false).orElse(null);
+ if (cloudFormation == null)
+ continue;
+ this.addCloud(cloudFormation, CloudGenerator.Order.USE_WEIGHT);
+ break;
+ }
+ }
+ }
+
+ protected void onRegionVisibilityChange(CloudRegion region, boolean nowVisible) {}
+
+ protected abstract List determineValidSpawnRegions(RandomSource random, @Nullable Level level);
+
+ public static enum Order
+ {
+ TOP((l, r) ->
+ {
+ l.add(r);
+ }),
+ BOTTOM((l, r) ->
+ {
+ l.add(0, r);
+ }),
+ USE_WEIGHT((l, r) ->
+ {
+ int prevWeight = 0;
+ for (int i = 0; i < l.size(); i++)
+ {
+ CloudRegion region = l.get(i);
+ if (r.getOrderWeight() >= prevWeight && r.getOrderWeight() <= region.getOrderWeight())
+ {
+ l.add(i, r);
+ prevWeight = region.getOrderWeight();
+ return;
+ }
+ }
+ l.add(r);
+ });
+
+ private final BiConsumer, CloudRegion> appender;
+
+ private Order(BiConsumer, CloudRegion> appender)
+ {
+ this.appender = appender;
+ }
+ }
+}
diff --git a/dev/nonamecrackers2/simpleclouds/common/world/CloudManager.java b/dev/nonamecrackers2/simpleclouds/common/world/CloudManager.java
new file mode 100644
index 00000000..f7c74bb7
--- /dev/null
+++ b/dev/nonamecrackers2/simpleclouds/common/world/CloudManager.java
@@ -0,0 +1,427 @@
+package dev.nonamecrackers2.simpleclouds.common.world;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+
+import dev.nonamecrackers2.simpleclouds.common.api.SimpleCloudsHooks;
+import org.apache.commons.lang3.tuple.Pair;
+import org.joml.Vector2f;
+
+import dev.nonamecrackers2.simpleclouds.api.SimpleCloudsAPI;
+import dev.nonamecrackers2.simpleclouds.api.common.cloud.CloudMode;
+import dev.nonamecrackers2.simpleclouds.api.common.cloud.weather.WeatherType;
+import dev.nonamecrackers2.simpleclouds.api.common.event.ModifyCloudSpeedEvent;
+import dev.nonamecrackers2.simpleclouds.api.common.world.ScAPICloudManager;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudTypeSource;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudGetter;
+import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudRegion;
+import dev.nonamecrackers2.simpleclouds.common.cloud.spawning.CloudGenerator;
+import dev.nonamecrackers2.simpleclouds.common.cloud.spawning.CloudSpawningConfig;
+import dev.nonamecrackers2.simpleclouds.common.config.SimpleCloudsConfig;
+import net.minecraft.core.BlockPos;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.dedicated.DedicatedServer;
+import net.minecraft.util.Mth;
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.biome.Biome;
+import net.minecraft.world.level.levelgen.Heightmap;
+import net.minecraftforge.common.MinecraftForge;
+
+public abstract class CloudManager implements CloudGetter, ScAPICloudManager
+{
+ public static final int CLOUD_HEIGHT_MAX = 2048;
+ public static final int CLOUD_HEIGHT_MIN = 0;
+ public static final int UPDATE_INTERVAL = 200;
+ public static final float RANDOM_SPREAD = 10000.0F;
+ public static final float SCROLL_OFFSET = 100.0F;
+ protected final T level;
+ protected final CloudTypeSource cloudSource;
+ protected final CloudGenerator cloudGenerator;
+ private long seed;
+ protected @Nullable RandomSource random;
+ protected float scrollAngle;
+ protected float scrollXO;
+ protected float scrollYO;
+ protected float scrollZO;
+ protected float scrollX;
+ protected float scrollY;
+ protected float scrollZ;
+ protected float speed = 1.0F;
+ protected int cloudHeight = 128;
+ protected int tickCount;
+ protected int nextLightningStrike = 60;
+ protected boolean useVanillaWeather;
+
+ @SuppressWarnings("unchecked")
+ public static CloudManager get(T level)
+ {
+ return Objects.requireNonNull(((CloudManagerHolder)level).getCloudManager(), "Cloud manager is not available, this shouldn't happen!");
+ }
+
+ public CloudManager(T level, CloudTypeSource source, Supplier configGetter, BiFunction, CloudGenerator> generatorFunc)
+ {
+ this.level = level;
+ this.cloudSource = source;
+ this.cloudGenerator = generatorFunc.apply(this, configGetter);
+ this.useVanillaWeather = this.determineUseVanillaWeather();
+ }
+
+ @Override
+ public CloudGenerator getCloudGenerator()
+ {
+ return this.cloudGenerator;
+ }
+
+ @Override
+ public List getClouds()
+ {
+ return this.cloudGenerator.getClouds();
+ }
+
+ @Override
+ public CloudType getCloudTypeForId(ResourceLocation id)
+ {
+ return this.cloudSource.getCloudTypeForId(id);
+ }
+
+ @Override
+ public CloudType[] getIndexedCloudTypes()
+ {
+ return this.cloudSource.getIndexedCloudTypes();
+ }
+
+ @Override
+ public boolean isCloudGeneratorActive() {
+ return this.getCloudMode() != CloudMode.SINGLE;
+ }
+
+
+ public void onPlayerJoin(Player player)
+ {
+ if (this.isCloudGeneratorActive() && !SimpleCloudsAPI.getApi().getHooks().isExternalWeatherControlEnabled())
+ this.cloudGenerator.doInitialGen(player.getBlockX(), player.getBlockZ(), this.level, false);
+ }
+
+ @Override
+ public Pair getCloudTypeAtPosition(float x, float z)
+ {
+ if (this.getCloudMode() != CloudMode.SINGLE)
+ {
+ Pair result = CloudRegion.calculateAt(this.getClouds(), x, z);
+ CloudType type = null;
+ if (result.getLeft() != null)
+ type = this.getCloudTypeForId(result.getLeft().getCloudTypeId());
+ if (type == null)
+ type = SimpleCloudsConstants.EMPTY;
+ return Pair.of(type, 1.0F - result.getRight());
+ }
+ else
+ {
+ String rawId = this.getSingleModeCloudTypeRawId();
+ ResourceLocation id = ResourceLocation.tryParse(rawId);
+ if (id != null)
+ {
+ CloudType type = this.getCloudTypeForId(id);
+ if (type != null)
+ return Pair.of(type, 0.0F);
+ }
+ return Pair.of(SimpleCloudsConstants.EMPTY, 0.0F);
+ }
+ }
+
+ public Pair getPrecipitationAt(BlockPos pos)
+ {
+ if (!this.level.canSeeSky(pos) || this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos).getY() > pos.getY())
+ return Pair.of(false, Biome.Precipitation.NONE);
+
+ Biome.Precipitation precipitation = this.level.getBiome(pos).value().getPrecipitationAt(pos);
+
+ var info = this.getCloudTypeAtWorldPos((float)pos.getX() + 0.5F, (float)pos.getZ() + 0.5F);
+ CloudType type = info.getLeft();
+ if ((float)pos.getY() + 0.5F > type.stormStart() * SimpleCloudsConstants.CLOUD_SCALE + 128.0F)
+ return Pair.of(false, Biome.Precipitation.NONE);
+
+ if (info.getLeft().weatherType().includesRain() && info.getRight() < SimpleCloudsConstants.RAIN_THRESHOLD - 0.01F)
+ return Pair.of(true, precipitation);
+ else
+ return Pair.of(false, Biome.Precipitation.NONE);
+ }
+
+ //For API calls, use Level#isRainingAt
+ public boolean isRainingAt(BlockPos pos)
+ {
+ Pair val = this.getPrecipitationAt(pos);
+ return val.getLeft() && val.getRight() != Biome.Precipitation.RAIN;
+ }
+
+ public boolean isSnowingAt(BlockPos pos)
+ {
+ Pair val = this.getPrecipitationAt(pos);
+ return val.getLeft() && val.getRight() == Biome.Precipitation.SNOW;
+ }
+
+ public boolean hasPrecipitationAt(BlockPos pos)
+ {
+ Pair val = this.getPrecipitationAt(pos);
+ return val.getLeft() && val.getRight() != Biome.Precipitation.NONE;
+ }
+
+ @Override
+ public float getRainLevel(float x, float y, float z)
+ {
+ var info = this.getCloudTypeAtWorldPos(x, z);
+ CloudType type = info.getLeft();
+
+ if (!type.weatherType().includesRain())
+ return 0.0F;
+
+ float fade = info.getRight();
+ float verticalFade = 1.0F - Mth.clamp((y - (type.stormStart() * SimpleCloudsConstants.CLOUD_SCALE + this.getCloudHeight())) / SimpleCloudsConstants.RAIN_VERTICAL_FADE, 0.0F, 1.0F);
+ return Math.min(1.0F, Math.max(0.0F, SimpleCloudsConstants.RAIN_THRESHOLD - fade) / SimpleCloudsConstants.RAIN_FADE) * verticalFade;
+ }
+
+ public void init(long seed)
+ {
+ RandomSource random = this.setSeed(seed);
+ this.random = random;
+ this.speed = 1.0F;
+ this.cloudGenerator.initialize(random, this.level);
+ }
+
+ @Override
+ public int getCloudHeight()
+ {
+ return this.cloudHeight;
+ }
+
+ @Override
+ public void setCloudHeight(int height)
+ {
+ this.cloudHeight = height;
+ }
+
+ public void tick()
+ {
+ MinecraftServer server = this.level.getServer();
+ if (server instanceof DedicatedServer && server.getPlayerCount() == 0)
+ return;
+
+ this.tickCount++;
+
+ this.scrollXO = this.scrollX;
+ this.scrollYO = this.scrollY;
+ this.scrollZO = this.scrollZ;
+ float speed = this.getCloudSpeed();
+ speed = this.modifyCloudSpeed(speed);
+
+ if (this.isCloudGeneratorActive())
+ this.cloudGenerator.tick(this.level, speed);
+
+ speed *= 0.0001F;
+ this.scrollAngle += speed;
+ this.scrollX = (float)Math.cos(this.scrollAngle) * SCROLL_OFFSET;
+ this.scrollY = 0.0F;//(float)Math.sin(this.scrollAngle + (float)Math.PI / 4.0F) * SCROLL_OFFSET * 0.5F;
+ this.scrollZ = (float)Math.sin(this.scrollAngle) * SCROLL_OFFSET;
+
+ boolean flag = this.determineUseVanillaWeather();
+ if (flag != this.useVanillaWeather)
+ {
+ this.useVanillaWeather = flag;
+ this.resetVanillaWeather();
+ }
+
+ if (!this.useVanillaWeather)
+ this.tickLightning();
+ }
+
+ protected void resetVanillaWeather() {}
+
+ protected void tickLightning()
+ {
+ if (this.nextLightningStrike <= 0 || --this.nextLightningStrike > 0)
+ return;
+ this.attemptToSpawnLightning();
+ int minInterval = SimpleCloudsConfig.COMMON.lightningSpawnIntervalMin.get();
+ int maxInterval = Math.max(minInterval, SimpleCloudsConfig.COMMON.lightningSpawnIntervalMax.get());
+ this.nextLightningStrike = Mth.randomBetweenInclusive(this.random, minInterval, maxInterval);
+ }
+
+ protected boolean determineUseVanillaWeather()
+ {
+ return useVanillaWeather(this.level, this);
+ }
+
+ @Override
+ public final boolean shouldUseVanillaWeather()
+ {
+ return this.useVanillaWeather;
+ }
+
+ protected abstract void attemptToSpawnLightning();
+
+ protected abstract void spawnLightning(CloudType type, float fade, int x, int z, boolean soundOnly);
+
+ @Override
+ public abstract CloudMode getCloudMode();
+
+ @Override
+ public abstract String getSingleModeCloudTypeRawId();
+
+ @Override
+ public void spawnLightning(int x, int z, boolean soundOnly)
+ {
+ var info = this.getCloudTypeAtWorldPos((float)x + 0.5F, (float)z + 0.5f);
+ this.spawnLightning(info.getLeft(), info.getRight(), x, z, soundOnly);
+ }
+
+ @Override
+ public Vector2f calculateWindDirection()
+ {
+ float dirX = Mth.cos(this.scrollAngle);
+ float dirZ = Mth.sin(this.scrollAngle);
+ return new Vector2f(dirX, dirZ);
+ }
+
+ @Override
+ public int getTickCount()
+ {
+ return this.tickCount;
+ }
+
+ @Override
+ public long getSeed()
+ {
+ return this.seed;
+ }
+
+ public RandomSource setSeed(long seed)
+ {
+ this.seed = seed;
+ return RandomSource.create(seed);
+ }
+
+ protected float modifyCloudSpeed(float speed)
+ {
+ ModifyCloudSpeedEvent event = new ModifyCloudSpeedEvent(this.level, this, speed);
+ MinecraftForge.EVENT_BUS.post(event);
+ return event.getCurrentSpeed();
+ }
+
+ @Override
+ public float getCloudSpeed()
+ {
+ return this.speed;
+ }
+
+ @Override
+ public void setCloudSpeed(float speed)
+ {
+ this.speed = Math.max(0.0F, speed);
+ }
+
+ @Override
+ public float getScrollAngle()
+ {
+ return this.scrollAngle;
+ }
+
+ @Override
+ public void setScrollAngle(float angle)
+ {
+ this.scrollAngle = angle;
+ }
+
+ @Override
+ public float getScrollX()
+ {
+ return this.scrollX;
+ }
+
+ @Override
+ public float getScrollY()
+ {
+ return this.scrollY;
+ }
+
+ @Override
+ public float getScrollZ()
+ {
+ return this.scrollZ;
+ }
+
+ @Override
+ public float getScrollX(float partialTicks)
+ {
+ return Mth.lerp(partialTicks, this.scrollXO, this.scrollX);
+ }
+
+ @Override
+ public float getScrollY(float partialTicks)
+ {
+ return Mth.lerp(partialTicks, this.scrollYO, this.scrollY);
+ }
+
+ @Override
+ public float getScrollZ(float partialTicks)
+ {
+ return Mth.lerp(partialTicks, this.scrollZO, this.scrollZ);
+ }
+
+ public static boolean isValidLightning(CloudType type, float fade, RandomSource random)
+ {
+ return type.weatherType().includesThunder() && fade < 0.8F;// && (fade > 0.7F || random.nextInt(3) == 0);
+ }
+
+ public static boolean useVanillaWeather(Level level, CloudTypeSource source)
+ {
+ if (!SimpleCloudsConfig.SERVER_SPEC.isLoaded())
+ return false;
+
+ boolean flag = SimpleCloudsConfig.SERVER.dimensionWhitelist.get().stream().anyMatch(val -> {
+ return level.dimension().location().toString().equals(val);
+ });
+
+ if (SimpleCloudsConfig.SERVER.whitelistAsBlacklist.get() ? flag : !flag)
+ return true;
+
+ CloudMode mode = SimpleCloudsConfig.SERVER.cloudMode.get();
+
+ switch (mode)
+ {
+ case AMBIENT:
+ {
+ return true;
+ }
+ case SINGLE:
+ {
+ String rawId = SimpleCloudsConfig.SERVER.singleModeCloudType.get();
+ ResourceLocation id = ResourceLocation.tryParse(rawId);
+ if (id != null)
+ {
+ CloudType type = source.getCloudTypeForId(id);
+ if (type != null && type.weatherType() == WeatherType.NONE)
+ return true;
+ }
+ }
+ default:
+ {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return this.getClass().getSimpleName() + "[level=" + this.level.dimension().location() + "]";
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/ClientRenderHook.java b/src/main/java/net/Gabou/projectatmosphere/client/ClientRenderHook.java
index cef4f1a2..bdd4e314 100644
--- a/src/main/java/net/Gabou/projectatmosphere/client/ClientRenderHook.java
+++ b/src/main/java/net/Gabou/projectatmosphere/client/ClientRenderHook.java
@@ -31,7 +31,7 @@ public static void onRender(RenderLevelStageEvent event) {
if (Minecraft.getInstance().level == null) return;
ClientLevel level = Minecraft.getInstance().level;
- List snapshot = new ArrayList<>(TornadoManager.getActiveTornadoes());
+ List snapshot = new ArrayList<>(TornadoManager.getClientTornadoes());
if (snapshot.isEmpty()) return;
PoseStack poseStack = event.getPoseStack();
Camera camera = Minecraft.getInstance().gameRenderer.getMainCamera();
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/ClientTickHandler.java b/src/main/java/net/Gabou/projectatmosphere/client/ClientTickHandler.java
index b9dd4efb..65e0376c 100644
--- a/src/main/java/net/Gabou/projectatmosphere/client/ClientTickHandler.java
+++ b/src/main/java/net/Gabou/projectatmosphere/client/ClientTickHandler.java
@@ -2,7 +2,7 @@
import net.Gabou.projectatmosphere.async.PoolType;
import net.Gabou.projectatmosphere.client.hurricane.ClientHurricaneStateCache;
-import net.Gabou.projectatmosphere.client.TornadoRenderHandler;
+import net.Gabou.projectatmosphere.client.TornadoClientEffects;
import net.Gabou.projectatmosphere.config.AtmoCommonConfig;
import net.Gabou.projectatmosphere.manager.ForecastOrchestrator;
import net.Gabou.projectatmosphere.modules.core.WindVector;
@@ -62,6 +62,7 @@ public static void onClientTick(TickEvent.ClientTickEvent event) {
if (!ClientSyncLock.isReady()) return;
if (mc.isPaused()) return;
if (mc.level == null) {
+ TornadoManager.clearClientTornadoes();
return;
}
@@ -88,7 +89,7 @@ public static void onClientTick(TickEvent.ClientTickEvent event) {
culledRegionIds.addAll(nextCulled);
}
- Set current = new HashSet<>(TornadoManager.getActiveTornadoes());
+ Set current = new HashSet<>(TornadoManager.getClientTornadoes());
for (TornadoInstance tornado : current) {
float baseVol = 0.35f + 0.45f * 0.75f;
TornadoAudioClient.ensure(tornado, baseVol, 140f);
@@ -102,8 +103,8 @@ public static void onClientTick(TickEvent.ClientTickEvent event) {
prevTornadoes.addAll(current);
if (mc.level.getGameTime() % 2 == 0) {
- for (TornadoInstance tornado : TornadoManager.getActiveTornadoes()) {
- TornadoRenderHandler.spawnDebrisParticles(tornado, (ClientLevel) mc.level);
+ for (TornadoInstance tornado : TornadoManager.getClientTornadoes()) {
+ TornadoClientEffects.spawnDebrisParticles(tornado, (ClientLevel) mc.level);
}
}
if (tickCounter % 40 == 0) {
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/TornadoClientEffects.java b/src/main/java/net/Gabou/projectatmosphere/client/TornadoClientEffects.java
new file mode 100644
index 00000000..c5fb7564
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/client/TornadoClientEffects.java
@@ -0,0 +1,45 @@
+package net.Gabou.projectatmosphere.client;
+
+import dev.nonamecrackers2.simpleclouds.common.config.SimpleCloudsConfig;
+import net.Gabou.projectatmosphere.modules.tornado.TornadoInstance;
+import net.Gabou.projectatmosphere.particles.DebrisParticleData;
+import net.minecraft.client.multiplayer.ClientLevel;
+
+public final class TornadoClientEffects {
+ private TornadoClientEffects() {
+ }
+
+ public static void spawnDebrisParticles(TornadoInstance tornado, ClientLevel level) {
+ double visualHeight = Math.min(tornado.getRenderHeight(1.0F), SimpleCloudsConfig.CLIENT.cloudHeight.get());
+ double maxRadius = Math.max(4.0, tornado.getRenderRadius(1.0F));
+ float intensity = tornado.getNormalizedIntensity();
+ float debrisScore = tornado.getRecentDebrisScore();
+
+ int lowCount = 7 + Math.round(intensity * 7.0F + debrisScore * 10.0F);
+ int midCount = 12 + Math.round(intensity * 8.0F + debrisScore * 7.0F);
+ int upperCount = 4 + Math.round(intensity * 4.0F + debrisScore * 2.0F);
+
+ spawnBand(level, tornado, lowCount, maxRadius * 1.24D, visualHeight * 0.20D, 8.4F);
+ spawnBand(level, tornado, midCount, maxRadius * 0.76D, visualHeight * 0.74D, 16.0F);
+ spawnBand(level, tornado, upperCount, maxRadius * 1.24D, visualHeight * 1.06D, 5.4F);
+ }
+
+ private static void spawnBand(ClientLevel level, TornadoInstance tornado, int count, double maxRadius, double maxHeight,
+ float angularSpeed) {
+ for (int i = 0; i < count; i++) {
+ double radius = Math.sqrt(level.random.nextDouble()) * Math.max(0.6D, maxRadius);
+ double height = level.random.nextDouble() * Math.max(1.0D, maxHeight);
+ float localAngularSpeed = (float) (angularSpeed * (0.82F + level.random.nextFloat() * 0.42F));
+
+ level.addParticle(
+ new DebrisParticleData(tornado, radius, height, localAngularSpeed),
+ tornado.getRenderPosition(1.0F).x,
+ tornado.getRenderBottomY(1.0F),
+ tornado.getRenderPosition(1.0F).z,
+ 0.0,
+ 0.0,
+ 0.0
+ );
+ }
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/hurricane/ClientHurricaneStateCache.java b/src/main/java/net/Gabou/projectatmosphere/client/hurricane/ClientHurricaneStateCache.java
index fe495952..d49f912c 100644
--- a/src/main/java/net/Gabou/projectatmosphere/client/hurricane/ClientHurricaneStateCache.java
+++ b/src/main/java/net/Gabou/projectatmosphere/client/hurricane/ClientHurricaneStateCache.java
@@ -1,7 +1,6 @@
package net.Gabou.projectatmosphere.client.hurricane;
import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
-import dev.nonamecrackers2.simpleclouds.common.world.CloudManager;
import net.Gabou.projectatmosphere.modules.hurricane.HurricaneManager;
import net.Gabou.projectatmosphere.modules.hurricane.HurricaneRenderSnapshot;
import net.minecraft.client.Minecraft;
@@ -56,9 +55,12 @@ public static List getSemanticSnapshots() {
public static List getSemanticSnapshots(float partialTick) {
Minecraft mc = Minecraft.getInstance();
- if (mc.level == null || ENTRIES.isEmpty()) {
+ if (mc.level == null) {
return List.of();
}
+ if (ENTRIES.isEmpty()) {
+ return projectatmosphere$getIntegratedServerSnapshots(mc);
+ }
long clientTick = mc.level.getGameTime();
List snapshots = new ArrayList<>(ENTRIES.size());
@@ -105,23 +107,18 @@ public static List getSemanticSnapshots(float partialTi
public static List getRenderableHurricanes(float partialTick) {
Minecraft mc = Minecraft.getInstance();
- if (mc.level == null || ENTRIES.isEmpty()) {
+ if (mc.level == null) {
return List.of();
}
- CloudManager> cloudManager = CloudManager.get(mc.level);
- int cloudHeight = cloudManager == null ? 0 : cloudManager.getCloudHeight();
List snapshots = getSemanticSnapshots(partialTick);
List renderables = new ArrayList<>(snapshots.size());
for (HurricaneRenderSnapshot snapshot : snapshots) {
- // Simple Clouds keeps vertical sampling in cloud-height-relative blocks, not cloud-scale units.
- float localAnchorY = snapshot.anchorY() - (float)cloudHeight;
-
renderables.add(new RenderableHurricane(
snapshot.id(),
snapshot.centerX() / (double)SimpleCloudsConstants.CLOUD_SCALE,
snapshot.centerZ() / (double)SimpleCloudsConstants.CLOUD_SCALE,
- localAnchorY,
+ snapshot.anchorY(),
snapshot.coreRadius() / (float)SimpleCloudsConstants.CLOUD_SCALE,
snapshot.stormExtentRadius() / (float)SimpleCloudsConstants.CLOUD_SCALE,
snapshot.eyeRadius() / (float)SimpleCloudsConstants.CLOUD_SCALE,
@@ -164,7 +161,3 @@ public record RenderableHurricane(
) {
}
}
-
-
-
-
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/render/SimpleCloudsRenderDiagnostics.java b/src/main/java/net/Gabou/projectatmosphere/client/render/SimpleCloudsRenderDiagnostics.java
new file mode 100644
index 00000000..06880d74
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/client/render/SimpleCloudsRenderDiagnostics.java
@@ -0,0 +1,93 @@
+package net.Gabou.projectatmosphere.client.render;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public final class SimpleCloudsRenderDiagnostics {
+ private static final Logger LOGGER = LogManager.getLogger("ProjectAtmosphere/SimpleCloudsRender");
+ private static final boolean ENABLED = Boolean.getBoolean("projectatmosphere.simpleclouds.debugRender");
+ private static final ThreadLocal CURRENT_PASS = ThreadLocal.withInitial(PassStats::new);
+
+ private SimpleCloudsRenderDiagnostics() {
+ }
+
+ public static boolean isEnabled() {
+ return ENABLED;
+ }
+
+ public static void beginPass(String passName, int totalChunks, int opaqueBytes, int transparentBytes, Object meshStatus) {
+ if (!ENABLED) {
+ return;
+ }
+
+ PassStats stats = CURRENT_PASS.get();
+ stats.passName = passName;
+ stats.totalChunks = totalChunks;
+ stats.opaqueBytes = opaqueBytes;
+ stats.transparentBytes = transparentBytes;
+ stats.meshStatus = meshStatus;
+ stats.drawCalls = 0;
+ stats.totalElements = 0;
+ stats.alphaFallbacks = 0;
+ }
+
+ public static void recordDraw(String passName, int elementCount) {
+ if (!ENABLED) {
+ return;
+ }
+
+ PassStats stats = CURRENT_PASS.get();
+ if (stats.passName == null || "unknown".equals(stats.passName)) {
+ stats.passName = passName;
+ }
+ stats.drawCalls++;
+ stats.totalElements += Math.max(0, elementCount);
+ }
+
+ public static void noteAlphaFallback(int elementCount, int ticksSinceLastGen) {
+ if (!ENABLED) {
+ return;
+ }
+
+ PassStats stats = CURRENT_PASS.get();
+ stats.alphaFallbacks++;
+ LOGGER.info(
+ "[SimpleCloudsRender] alpha fallback triggered elementCount={} ticksSinceLastGen={} pass={} drawCalls={} totalElements={}",
+ elementCount,
+ ticksSinceLastGen,
+ stats.passName,
+ stats.drawCalls,
+ stats.totalElements
+ );
+ }
+
+ public static void endPass() {
+ if (!ENABLED) {
+ return;
+ }
+
+ PassStats stats = CURRENT_PASS.get();
+ LOGGER.info(
+ "[SimpleCloudsRender] pass={} totalChunks={} drawCalls={} totalElements={} alphaFallbacks={} opaqueBytes={} transparentBytes={} meshStatus={}",
+ stats.passName,
+ stats.totalChunks,
+ stats.drawCalls,
+ stats.totalElements,
+ stats.alphaFallbacks,
+ stats.opaqueBytes,
+ stats.transparentBytes,
+ stats.meshStatus
+ );
+ }
+
+ private static final class PassStats {
+ private String passName = "unknown";
+ private int totalChunks;
+ private int opaqueBytes;
+ private int transparentBytes;
+ private Object meshStatus;
+ private int drawCalls;
+ private int totalElements;
+ private int alphaFallbacks;
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/render/SimpleCloudsTornadoRenderer.java b/src/main/java/net/Gabou/projectatmosphere/client/render/SimpleCloudsTornadoRenderer.java
new file mode 100644
index 00000000..4ce49421
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/client/render/SimpleCloudsTornadoRenderer.java
@@ -0,0 +1,722 @@
+package net.Gabou.projectatmosphere.client.render;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.PoseStack;
+import dev.nonamecrackers2.simpleclouds.client.renderer.SimpleCloudsRenderer;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.common.world.CloudManager;
+import net.Gabou.projectatmosphere.ProjectAtmosphere;
+import net.Gabou.projectatmosphere.modules.tornado.TornadoInstance;
+import net.Gabou.projectatmosphere.modules.tornado.TornadoManager;
+import net.Gabou.projectatmosphere.modules.weather.StormLifecyclePhase;
+import net.Gabou.projectatmosphere.config.AtmoCommonConfig;
+import net.minecraft.client.Camera;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.renderer.ShaderInstance;
+import net.minecraft.world.level.levelgen.Heightmap;
+import net.minecraft.client.renderer.texture.AbstractTexture;
+import net.minecraft.util.Mth;
+import net.minecraft.world.phys.Vec3;
+import org.joml.Matrix4f;
+import org.joml.Vector4f;
+import org.lwjgl.opengl.GL11;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+public final class SimpleCloudsTornadoRenderer {
+ public static final SimpleCloudsTornadoRenderer INSTANCE = new SimpleCloudsTornadoRenderer();
+
+ private static final int MAX_STORMS = 8;
+ private static final float CLOUD_BLEND_PAD_ABOVE_CLOUD_BASE_WORLD = 28.0F;
+ private static final float GROUND_CONTACT_EXTENSION_WORLD = 12.0F;
+ private static final float GROUND_CONTACT_PADDING_WORLD = 2.0F;
+ private static final float MIN_VISUAL_WORLD_WIDTH = 28.0F;
+ private static final float MIN_VISUAL_WORLD_STORM_SIZE = 140.0F;
+ private static final float MIN_VISUAL_WORLD_HEIGHT = 120.0F;
+ private static final float MAX_RAY_DISTANCE_CLOUD = 420.0F;
+ private static final float WHITEOUT_STRENGTH = 0.40F;
+ private static final float WHITEOUT_THRESHOLD = 0.12F;
+ private static final float RAY_STEP_CLOUD = 0.42F;
+ private static final float WALLCLOUD_LOWER_WORLD = 15.0F;
+ private static final float FUNNEL_TOP_OFFSET_WORLD = 13.125F;
+ private static final float FUNNEL_BASE_PADDING_WORLD = 3.75F;
+ private static final float WALLCLOUD_GATE_BELOW_ORIGIN_WORLD = 8.5F;
+ private static final float TOUCHDOWN_TOP_BLEND_WORLD = 3.75F;
+ private static final float CONNECTION_BLEND_WORLD = 1.8F;
+
+ private ClientLevel preparedLevel;
+ private long preparedGameTime = Long.MIN_VALUE;
+ private float preparedPartialTick = Float.NaN;
+ private final List preparedTornadoes = new ArrayList<>();
+ private final VolumeBoxMesh volumeBox = new VolumeBoxMesh();
+ private int resolvedDebugStormIndex = -1;
+ private long lastRenderOpaqueLogGameTime = Long.MIN_VALUE;
+ private long lastRenderTransparencyLogGameTime = Long.MIN_VALUE;
+ private long lastDiagnosticReportGameTime = Long.MIN_VALUE;
+
+ private SimpleCloudsTornadoRenderer() {
+ }
+
+ public void prepareFrame(ClientLevel level, float partialTick) {
+ if (this.preparedLevel == level
+ && this.preparedGameTime == level.getGameTime()
+ && Float.compare(this.preparedPartialTick, partialTick) == 0) {
+ return;
+ }
+
+ this.preparedLevel = level;
+ this.preparedGameTime = level.getGameTime();
+ this.preparedPartialTick = partialTick;
+ this.preparedTornadoes.clear();
+
+ float animationTime = TornadoManager.getShaderTime() + partialTick * 0.05F;
+ for (TornadoInstance tornado : TornadoManager.getClientTornadoes()) {
+ if (this.preparedTornadoes.size() >= MAX_STORMS) {
+ break;
+ }
+ this.preparedTornadoes.add(PreparedTornado.from(level, tornado, animationTime, partialTick));
+ }
+
+ Camera camera = Minecraft.getInstance().gameRenderer.getMainCamera();
+ if (camera != null && TornadoRenderDebugState.isActive()) {
+ this.resolvedDebugStormIndex = this.resolveDebugStormIndex(
+ camera.getPosition(),
+ new Vec3(camera.getLookVector())
+ );
+ } else {
+ this.resolvedDebugStormIndex = -1;
+ }
+
+ if (shouldDebugLog(level)) {
+ debug(
+ "prepareFrame complete gameTime={} tornadoes={} debugState={} resolvedDebugStorm={}",
+ level.getGameTime(),
+ this.preparedTornadoes.size(),
+ TornadoRenderDebugState.describe(),
+ this.resolvedDebugStormIndex
+ );
+ }
+ }
+
+ public void renderOpaque(SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat,
+ float partialTick, float cloudR, float cloudG, float cloudB) {
+ this.renderOpaque(renderer, stack, projMat, partialTick, cloudR, cloudG, cloudB, renderer.getCloudTarget().getDepthTextureId(), true);
+ }
+
+ public void renderOpaque(SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat,
+ float partialTick, float cloudR, float cloudG, float cloudB,
+ int depthTextureId, boolean writeDepth) {
+ ClientLevel level = Minecraft.getInstance().level;
+ if (shouldDebugLog(level) && level != null && this.lastRenderOpaqueLogGameTime != level.getGameTime()) {
+ this.lastRenderOpaqueLogGameTime = level.getGameTime();
+ debug(
+ "renderOpaque called gameTime={} tornadoes={} shaderReady={} debugState={} resolvedDebugStorm={}",
+ level.getGameTime(),
+ this.preparedTornadoes.size(),
+ TornadoShaders.isReady(),
+ TornadoRenderDebugState.describe(),
+ this.resolvedDebugStormIndex
+ );
+ }
+ if (this.preparedTornadoes.isEmpty() || !TornadoShaders.isReady()) {
+ return;
+ }
+
+ Minecraft mc = Minecraft.getInstance();
+ ShaderInstance shader = TornadoShaders.getShader();
+ if (shader == null) {
+ return;
+ }
+
+ RenderSystem.enableBlend();
+ RenderSystem.defaultBlendFunc();
+ // Keep the volume depth-aware so it sits inside the world instead of reading like a flat overlay.
+ // The shader still raymarchs against its own max distance rather than using copied scene depth as a
+ // hard clip plane, which preserves the earlier horizon fix over water and long flat terrain.
+ RenderSystem.enableDepthTest();
+ RenderSystem.depthMask(writeDepth);
+ RenderSystem.depthFunc(GL11.GL_LEQUAL);
+ RenderSystem.disableCull();
+ RenderSystem.setShader(() -> shader);
+
+ AbstractTexture tornadoTexture = mc.getTextureManager().getTexture(TornadoShaders.TORNADO_TEXTURE);
+ AbstractTexture noiseTexture = mc.getTextureManager().getTexture(TornadoShaders.NOISE_TEXTURE);
+ AbstractTexture flowTexture = mc.getTextureManager().getTexture(TornadoShaders.FLOW_TEXTURE);
+ shader.setSampler("TornadoSampler", tornadoTexture);
+ shader.setSampler("NoiseSampler", noiseTexture);
+ shader.setSampler("FlowSampler", flowTexture);
+ shader.setSampler("DepthSampler", depthTextureId);
+
+ shader.safeGetUniform("ModelViewMat").set(stack.last().pose());
+ shader.safeGetUniform("ProjMat").set(projMat);
+ Matrix4f inverseProj = new Matrix4f(projMat).invert();
+ Matrix4f inverseModelView = new Matrix4f(stack.last().pose()).invert();
+ shader.safeGetUniform("InverseProjMat").set(inverseProj);
+ shader.safeGetUniform("InverseModelViewMat").set(inverseModelView);
+
+ float scale = SimpleCloudsConstants.CLOUD_SCALE;
+ float cloudHeight = CloudManager.get(level).getCloudHeight();
+ Vec3 cameraPos = mc.gameRenderer.getMainCamera().getPosition();
+ Vec3 cameraPosCloud = new Vec3(
+ cameraPos.x / scale,
+ (cameraPos.y - cloudHeight) / scale,
+ cameraPos.z / scale
+ );
+ shader.safeGetUniform("CameraPos").set((float) cameraPosCloud.x, (float) cameraPosCloud.y, (float) cameraPosCloud.z);
+
+ TornadoRenderDebugState.Mode debugMode = TornadoRenderDebugState.isActive()
+ ? TornadoRenderDebugState.getMode()
+ : TornadoRenderDebugState.Mode.OFF;
+ shader.safeGetUniform("CloudScale").set(scale);
+ shader.safeGetUniform("RenderQuality").set((float) AtmoCommonConfig.TORNADO_RENDER_QUALITY.get().doubleValue());
+
+ shader.safeGetUniform("CloudColor").set(cloudR, cloudG, cloudB, 1.0F);
+ shader.safeGetUniform("AnimationTime").set(TornadoManager.getShaderTime() + partialTick * 0.05F);
+ shader.safeGetUniform("MaxDistance").set(MAX_RAY_DISTANCE_CLOUD);
+ shader.safeGetUniform("OutSize").set((float) mc.getWindow().getWidth(), (float) mc.getWindow().getHeight());
+ shader.safeGetUniform("FogStart").set(renderer.getFogStart());
+ shader.safeGetUniform("FogEnd").set(renderer.getFogEnd());
+ float[] fogColor = RenderSystem.getShaderFogColor();
+ shader.safeGetUniform("FogColor").set(fogColor[0], fogColor[1], fogColor[2], fogColor[3]);
+
+ this.maybeEmitDiagnosticReport(level, inverseProj, inverseModelView, cameraPos, cameraPosCloud, writeDepth);
+
+ List renderOrder = new ArrayList<>();
+ for (int i = 0; i < this.preparedTornadoes.size(); i++) {
+ if (debugMode != TornadoRenderDebugState.Mode.OFF && i != this.resolvedDebugStormIndex) {
+ continue;
+ }
+ renderOrder.add(i);
+ }
+ renderOrder.sort((left, right) -> Double.compare(
+ this.preparedTornadoes.get(right).centerWorld().distanceToSqr(cameraPos),
+ this.preparedTornadoes.get(left).centerWorld().distanceToSqr(cameraPos)
+ ));
+
+ for (int index : renderOrder) {
+ PreparedTornado tornado = this.preparedTornadoes.get(index);
+ this.applyStormUniforms(shader, tornado);
+ shader.safeGetUniform("DebugMode").set(debugMode.shaderValue());
+ shader.safeGetUniform("DebugSelectedStorm").set(debugMode == TornadoRenderDebugState.Mode.OFF ? -1 : 0);
+ shader.safeGetUniform("DebugFreeze").set(TornadoRenderDebugState.isFreezeEnabled() ? 1 : 0);
+ shader.safeGetUniform("VolumeMin").set(
+ (float) tornado.boundsMinCloud().x,
+ (float) tornado.boundsMinCloud().y,
+ (float) tornado.boundsMinCloud().z
+ );
+ shader.safeGetUniform("VolumeMax").set(
+ (float) tornado.boundsMaxCloud().x,
+ (float) tornado.boundsMaxCloud().y,
+ (float) tornado.boundsMaxCloud().z
+ );
+ shader.apply();
+ this.volumeBox.draw(shader, stack.last().pose(), projMat);
+ shader.clear();
+ }
+
+ RenderSystem.depthMask(true);
+ RenderSystem.enableDepthTest();
+ RenderSystem.disableBlend();
+ RenderSystem.enableCull();
+ }
+
+ public void renderTransparency(SimpleCloudsRenderer renderer, PoseStack stack, Matrix4f projMat,
+ float partialTick, float cloudR, float cloudG, float cloudB) {
+ ClientLevel level = Minecraft.getInstance().level;
+ if (shouldDebugLog(level) && level != null && this.lastRenderTransparencyLogGameTime != level.getGameTime()) {
+ this.lastRenderTransparencyLogGameTime = level.getGameTime();
+ debug("renderTransparency called gameTime={} tornadoes={}", level.getGameTime(), this.preparedTornadoes.size());
+ }
+ }
+
+ private void applyStormUniforms(ShaderInstance shader, PreparedTornado tornado) {
+ float[] stormPositions = new float[MAX_STORMS * 3];
+ float[] stormHeights = new float[MAX_STORMS];
+ float[] stormWidths = new float[MAX_STORMS];
+ float[] stormSizes = new float[MAX_STORMS];
+ float[] stormSpins = new float[MAX_STORMS];
+ float[] stormIntensities = new float[MAX_STORMS];
+ float[] stormShapes = new float[MAX_STORMS];
+ float[] stormProgress = new float[MAX_STORMS];
+
+ stormPositions[0] = tornado.centerX();
+ stormPositions[1] = tornado.bottomY();
+ stormPositions[2] = tornado.centerZ();
+ stormHeights[0] = tornado.height();
+ stormWidths[0] = tornado.width();
+ stormSizes[0] = tornado.stormSize();
+ stormSpins[0] = tornado.spin();
+ stormIntensities[0] = tornado.intensity();
+ stormShapes[0] = tornado.shape();
+ stormProgress[0] = tornado.touchdownProgress();
+
+ shader.safeGetUniform("StormCount").set(1);
+ shader.safeGetUniform("StormPositions").set(stormPositions);
+ shader.safeGetUniform("StormHeights").set(stormHeights);
+ shader.safeGetUniform("StormWidths").set(stormWidths);
+ shader.safeGetUniform("StormSizes").set(stormSizes);
+ shader.safeGetUniform("StormSpins").set(stormSpins);
+ shader.safeGetUniform("StormIntensities").set(stormIntensities);
+ shader.safeGetUniform("StormShapes").set(stormShapes);
+ shader.safeGetUniform("StormProgress").set(stormProgress);
+ }
+
+ public float sampleWhiteoutAtCamera(ClientLevel level, Vec3 cameraPos, float partialTick) {
+ if (TornadoRenderDebugState.isActive()) {
+ return 0.0F;
+ }
+
+ float scale = SimpleCloudsConstants.CLOUD_SCALE;
+ float cloudHeight = CloudManager.get(level).getCloudHeight();
+ float sampleX = (float) cameraPos.x / scale;
+ float sampleY = ((float) cameraPos.y - cloudHeight) / scale;
+ float sampleZ = (float) cameraPos.z / scale;
+ float strongest = 0.0F;
+ float animationTime = TornadoManager.getShaderTime() + partialTick * 0.05F;
+
+ for (TornadoInstance tornado : TornadoManager.getClientTornadoes()) {
+ PreparedTornado prepared = PreparedTornado.from(level, tornado, animationTime, partialTick);
+ float density = sampleAnalyticalDensity(sampleX, sampleY, sampleZ, prepared);
+ float whiteout = Mth.clamp((density - WHITEOUT_THRESHOLD) / 0.18F, 0.0F, 1.0F) * WHITEOUT_STRENGTH;
+ strongest = Math.max(strongest, whiteout);
+ }
+ return strongest;
+ }
+
+ public void close() {
+ this.preparedLevel = null;
+ this.preparedGameTime = Long.MIN_VALUE;
+ this.preparedPartialTick = Float.NaN;
+ this.preparedTornadoes.clear();
+ this.resolvedDebugStormIndex = -1;
+ this.volumeBox.close();
+ }
+
+ private int resolveDebugStormIndex(Vec3 cameraPosWorld, Vec3 cameraLook) {
+ int requestedStormIndex = TornadoRenderDebugState.getRequestedStormIndex();
+ if (requestedStormIndex >= 0 && requestedStormIndex < this.preparedTornadoes.size()) {
+ return requestedStormIndex;
+ }
+ if (this.preparedTornadoes.isEmpty()) {
+ return -1;
+ }
+
+ int bestVisible = -1;
+ double bestVisibleDistanceSqr = Double.MAX_VALUE;
+ int bestOverall = 0;
+ double bestOverallDistanceSqr = Double.MAX_VALUE;
+ Vec3 normalizedLook = cameraLook.lengthSqr() > 0.0D ? cameraLook.normalize() : new Vec3(0.0D, 0.0D, 1.0D);
+
+ for (int i = 0; i < this.preparedTornadoes.size(); i++) {
+ PreparedTornado tornado = this.preparedTornadoes.get(i);
+ Vec3 tornadoCenter = tornado.centerWorld();
+ Vec3 toStorm = tornadoCenter.subtract(cameraPosWorld);
+ double distanceSqr = toStorm.lengthSqr();
+ if (distanceSqr < bestOverallDistanceSqr) {
+ bestOverallDistanceSqr = distanceSqr;
+ bestOverall = i;
+ }
+ if (distanceSqr <= 0.0001D) {
+ continue;
+ }
+
+ double dot = toStorm.normalize().dot(normalizedLook);
+ if (dot > 0.15D && distanceSqr < bestVisibleDistanceSqr) {
+ bestVisibleDistanceSqr = distanceSqr;
+ bestVisible = i;
+ }
+ }
+ return bestVisible >= 0 ? bestVisible : bestOverall;
+ }
+
+ private void maybeEmitDiagnosticReport(ClientLevel level, Matrix4f inverseProj, Matrix4f inverseModelView,
+ Vec3 cameraPosWorld, Vec3 cameraPosCloud, boolean writeDepth) {
+ boolean requested = TornadoRenderDebugState.consumeDiagnosticReportRequest();
+ boolean periodic = TornadoRenderDebugState.isActive()
+ && shouldDebugLog(level)
+ && this.lastDiagnosticReportGameTime != level.getGameTime();
+ if (!requested && !periodic) {
+ return;
+ }
+
+ this.lastDiagnosticReportGameTime = level.getGameTime();
+ debug(
+ "renderState mode={} freeze={} writeDepth={} blend=srcalpha,1-srcalpha depthTest=LEQUAL cull=disabled proxyVolume=true resolvedStorm={}",
+ TornadoRenderDebugState.getMode().token(),
+ TornadoRenderDebugState.isFreezeEnabled(),
+ writeDepth,
+ this.resolvedDebugStormIndex
+ );
+
+ if (this.resolvedDebugStormIndex < 0 || this.resolvedDebugStormIndex >= this.preparedTornadoes.size()) {
+ debug("diagnostic skipped: no selected tornado. preparedCount={}", this.preparedTornadoes.size());
+ return;
+ }
+
+ PreparedTornado tornado = this.preparedTornadoes.get(this.resolvedDebugStormIndex);
+ CenterRayDiagnostic diagnostic = sampleCenterRayDiagnostic(tornado, inverseProj, inverseModelView, cameraPosCloud);
+ debug(
+ "selectedStorm index={} id={} renderPosWorld=({}, {}, {}) cloudHeightWorld={} cloudScale={} renderBottomWorld={} terrainSurfaceWorld={} bottomWorld={} topWorld={} bottomYCloud={} heightCloud={} heightWorld={} widthCloud={} widthWorld={} stormSizeCloud={} stormSizeWorld={} boundsRadiusCloud={} boundsRadiusWorld={} wallcloudRadiusWorld={}",
+ this.resolvedDebugStormIndex,
+ tornado.id(),
+ fmt(tornado.renderPosWorld().x), fmt(tornado.renderPosWorld().y), fmt(tornado.renderPosWorld().z),
+ fmt(tornado.cloudHeightWorld()),
+ fmt(tornado.scale()),
+ fmt(tornado.renderBottomWorld()),
+ fmt(tornado.terrainSurfaceWorld()),
+ fmt(tornado.bottomWorld()),
+ fmt(tornado.topWorld()),
+ fmt(tornado.bottomY()),
+ fmt(tornado.height()),
+ fmt(tornado.heightWorld()),
+ fmt(tornado.width()),
+ fmt(tornado.widthWorld()),
+ fmt(tornado.stormSize()),
+ fmt(tornado.stormSizeWorld()),
+ fmt(tornado.boundsRadiusCloud()),
+ fmt(tornado.boundsRadiusWorld()),
+ fmt(tornado.wallcloudRadiusWorld())
+ );
+
+ if (diagnostic == null) {
+ debug(
+ "centerRay cameraWorld=({}, {}, {}) cameraCloud=({}, {}, {}) note=center ray did not intersect selected tornado AABB",
+ fmt(cameraPosWorld.x), fmt(cameraPosWorld.y), fmt(cameraPosWorld.z),
+ fmt(cameraPosCloud.x), fmt(cameraPosCloud.y), fmt(cameraPosCloud.z)
+ );
+ return;
+ }
+
+ debug(
+ "centerRay cameraWorld=({}, {}, {}) cameraCloud=({}, {}, {}) rayEndCloud=({}, {}, {}) rayDirCloud=({}, {}, {}) tNear={} tFar={} stepSize={} samplePosCloud=({}, {}, {}) samplePosWorld=({}, {}, {}) tornadoOriginCloud=({}, {}, {}) tornadoOriginWorld=({}, {}, {}) localPosCloud=({}, {}, {}) localPosWorld=({}, {}, {}) radialDistanceWorld={} height01={} heightMask={} funnelRadiusWorld={} density={} alpha={} wallcloudRadiusWorld={} wallcloudLowerWorld={} connectionRadiusWorld={}",
+ fmt(cameraPosWorld.x), fmt(cameraPosWorld.y), fmt(cameraPosWorld.z),
+ fmt(cameraPosCloud.x), fmt(cameraPosCloud.y), fmt(cameraPosCloud.z),
+ fmt(diagnostic.rayEndCloud().x), fmt(diagnostic.rayEndCloud().y), fmt(diagnostic.rayEndCloud().z),
+ fmt(diagnostic.rayDirectionCloud().x), fmt(diagnostic.rayDirectionCloud().y), fmt(diagnostic.rayDirectionCloud().z),
+ fmt(diagnostic.tNear()),
+ fmt(diagnostic.tFar()),
+ fmt(diagnostic.stepSize()),
+ fmt(diagnostic.samplePosCloud().x), fmt(diagnostic.samplePosCloud().y), fmt(diagnostic.samplePosCloud().z),
+ fmt(diagnostic.samplePosWorld().x), fmt(diagnostic.samplePosWorld().y), fmt(diagnostic.samplePosWorld().z),
+ fmt(diagnostic.tornadoOriginCloud().x), fmt(diagnostic.tornadoOriginCloud().y), fmt(diagnostic.tornadoOriginCloud().z),
+ fmt(diagnostic.tornadoOriginWorld().x), fmt(diagnostic.tornadoOriginWorld().y), fmt(diagnostic.tornadoOriginWorld().z),
+ fmt(diagnostic.localPosCloud().x), fmt(diagnostic.localPosCloud().y), fmt(diagnostic.localPosCloud().z),
+ fmt(diagnostic.localPosWorld().x), fmt(diagnostic.localPosWorld().y), fmt(diagnostic.localPosWorld().z),
+ fmt(diagnostic.funnelSample().radialDistanceWorld()),
+ fmt(diagnostic.funnelSample().height01()),
+ fmt(diagnostic.funnelSample().heightMask()),
+ fmt(diagnostic.funnelSample().funnelRadiusWorld()),
+ fmt(diagnostic.funnelSample().density()),
+ fmt(diagnostic.funnelSample().alpha()),
+ fmt(diagnostic.funnelSample().wallcloudRadiusWorld()),
+ fmt(diagnostic.funnelSample().wallcloudLowerWorld()),
+ fmt(diagnostic.funnelSample().connectionRadiusWorld())
+ );
+ }
+
+ private static CenterRayDiagnostic sampleCenterRayDiagnostic(PreparedTornado tornado, Matrix4f inverseProj,
+ Matrix4f inverseModelView, Vec3 cameraPosCloud) {
+ Vec3 rayEndCloud = reconstructPosition(0.5F, 0.5F, 1.0F, inverseProj, inverseModelView);
+ Vec3 rayDirectionCloud = rayEndCloud.subtract(cameraPosCloud);
+ if (rayDirectionCloud.lengthSqr() <= 0.000001D) {
+ return null;
+ }
+ rayDirectionCloud = rayDirectionCloud.normalize();
+
+ Vec3 boundsMin = new Vec3(
+ tornado.centerX() - tornado.boundsRadiusCloud(),
+ tornado.bottomY() - (8.0F / tornado.scale()),
+ tornado.centerZ() - tornado.boundsRadiusCloud()
+ );
+ Vec3 boundsMax = new Vec3(
+ tornado.centerX() + tornado.boundsRadiusCloud(),
+ tornado.bottomY() + tornado.height() + (12.0F / tornado.scale()),
+ tornado.centerZ() + tornado.boundsRadiusCloud()
+ );
+ AabbHit hit = intersectAabb(cameraPosCloud, rayDirectionCloud, boundsMin, boundsMax);
+ if (hit == null) {
+ return null;
+ }
+
+ float interval = hit.far() - hit.near();
+ int steps = Mth.clamp(Mth.floor(interval / RAY_STEP_CLOUD), 18, 52);
+ float stepSize = interval / Math.max(steps, 1);
+ float jitter = hash1(0.5F * Minecraft.getInstance().getWindow().getWidth()
+ + 0.5F * Minecraft.getInstance().getWindow().getHeight()
+ + tornado.seed() * 17.13F);
+ float t = hit.near() + stepSize * (0.20F + jitter * 0.80F);
+
+ Vec3 samplePosCloud = cameraPosCloud.add(rayDirectionCloud.scale(t));
+ Vec3 samplePosWorld = new Vec3(
+ samplePosCloud.x * tornado.scale(),
+ samplePosCloud.y * tornado.scale() + tornado.cloudHeightWorld(),
+ samplePosCloud.z * tornado.scale()
+ );
+ Vec3 tornadoOriginCloud = tornado.originCloud();
+ Vec3 tornadoOriginWorld = tornado.originWorld();
+ Vec3 localPosCloud = samplePosCloud.subtract(tornadoOriginCloud);
+ Vec3 localPosWorld = samplePosWorld.subtract(tornadoOriginWorld);
+ return new CenterRayDiagnostic(
+ rayEndCloud,
+ rayDirectionCloud,
+ hit.near(),
+ hit.far(),
+ stepSize,
+ samplePosCloud,
+ samplePosWorld,
+ tornadoOriginCloud,
+ tornadoOriginWorld,
+ localPosCloud,
+ localPosWorld,
+ sampleDeterministicFunnel(samplePosCloud, tornado)
+ );
+ }
+
+ private static Vec3 reconstructPosition(float u, float v, float depth, Matrix4f inverseProj, Matrix4f inverseModelView) {
+ Vector4f ndc = new Vector4f(u * 2.0F - 1.0F, v * 2.0F - 1.0F, depth * 2.0F - 1.0F, 1.0F);
+ inverseProj.transform(ndc);
+ ndc.div(ndc.w);
+ inverseModelView.transform(ndc);
+ ndc.div(ndc.w);
+ return new Vec3(ndc.x, ndc.y, ndc.z);
+ }
+
+ private static AabbHit intersectAabb(Vec3 ro, Vec3 rd, Vec3 bmin, Vec3 bmax) {
+ double invX = 1.0D / rd.x;
+ double invY = 1.0D / rd.y;
+ double invZ = 1.0D / rd.z;
+
+ double t0x = (bmin.x - ro.x) * invX;
+ double t1x = (bmax.x - ro.x) * invX;
+ double t0y = (bmin.y - ro.y) * invY;
+ double t1y = (bmax.y - ro.y) * invY;
+ double t0z = (bmin.z - ro.z) * invZ;
+ double t1z = (bmax.z - ro.z) * invZ;
+
+ double minX = Math.min(t0x, t1x);
+ double minY = Math.min(t0y, t1y);
+ double minZ = Math.min(t0z, t1z);
+ double maxX = Math.max(t0x, t1x);
+ double maxY = Math.max(t0y, t1y);
+ double maxZ = Math.max(t0z, t1z);
+
+ double tNear = Math.max(Math.max(minX, minY), minZ);
+ double tFar = Math.min(Math.min(maxX, maxY), maxZ);
+ if (tFar <= Math.max(tNear, 0.0D)) {
+ return null;
+ }
+ return new AabbHit((float) Math.max(tNear, 0.0D), (float) tFar);
+ }
+
+ private static float hash1(float p) {
+ float value = Mth.frac(p * 0.1031F);
+ value *= value + 33.33F;
+ value *= value + value;
+ return Mth.frac(value);
+ }
+
+ private static float sampleAnalyticalDensity(float sampleX, float sampleY, float sampleZ, PreparedTornado tornado) {
+ return sampleDeterministicFunnel(new Vec3(sampleX, sampleY, sampleZ), tornado).density();
+ }
+
+ private static DeterministicFunnelSample sampleDeterministicFunnel(Vec3 samplePosCloud, PreparedTornado tornado) {
+ float sampleXWorld = (float) samplePosCloud.x * tornado.scale();
+ float sampleYWorld = (float) samplePosCloud.y * tornado.scale() + tornado.cloudHeightWorld();
+ float sampleZWorld = (float) samplePosCloud.z * tornado.scale();
+ float localXWorld = sampleXWorld - (float) tornado.renderPosWorld().x;
+ float localZWorld = sampleZWorld - (float) tornado.renderPosWorld().z;
+ float localYWorld = sampleYWorld - tornado.bottomWorld();
+ float topWorld = tornado.topWorld();
+ float funnelTopWorld = Math.max(topWorld - FUNNEL_TOP_OFFSET_WORLD, tornado.bottomWorld() + FUNNEL_BASE_PADDING_WORLD);
+ float height01 = Mth.clamp(localYWorld / Math.max(tornado.heightWorld(), 0.001F), 0.0F, 1.0F);
+ float heightMask = Mth.clamp((sampleYWorld - tornado.bottomWorld()) / Math.max(funnelTopWorld - tornado.bottomWorld(), 0.001F), 0.0F, 1.0F);
+ float funnelRadiusWorld = sampleFunnelRadiusWorld(tornado, sampleYWorld, funnelTopWorld);
+ float radialDistanceWorld = Mth.sqrt(localXWorld * localXWorld + localZWorld * localZWorld);
+ float radialMask = 1.0F - Mth.clamp(radialDistanceWorld / Math.max(funnelRadiusWorld, 0.001F), 0.0F, 1.0F);
+ float density = 0.0F;
+ if (sampleYWorld >= tornado.bottomWorld() && sampleYWorld <= funnelTopWorld) {
+ density = radialMask;
+ }
+ float alpha = Mth.clamp(density * 0.85F, 0.0F, 1.0F);
+ float wallcloudLowerWorld = WALLCLOUD_LOWER_WORLD
+ * (float) Math.pow(Math.max(0.0F, 1.0F - Mth.clamp(radialDistanceWorld / Math.max(tornado.wallcloudRadiusWorld(), 0.001F), 0.0F, 1.0F)), 0.25F)
+ * Mth.clamp((tornado.intensity() - 0.45F) * 2.2F, 0.0F, 1.0F);
+ float connectionRadiusWorld = Math.max(
+ funnelRadiusWorld * Mth.lerp(tornado.intensity(), 1.8F, 2.5F),
+ tornado.stormSizeWorld() * 0.28F
+ );
+ return new DeterministicFunnelSample(height01, heightMask, radialMask, funnelRadiusWorld, density, alpha,
+ tornado.wallcloudRadiusWorld(), wallcloudLowerWorld, connectionRadiusWorld);
+ }
+
+ private static float sampleFunnelRadiusWorld(PreparedTornado tornado, float sampleYWorld, float funnelTopWorld) {
+ float widthWorld = tornado.widthWorld();
+ float stormSizeWorld = tornado.stormSizeWorld();
+ float percFunnelHeight = Mth.clamp(
+ (sampleYWorld - tornado.bottomWorld()) / Math.max(funnelTopWorld - tornado.bottomWorld(), 0.001F),
+ 0.0F,
+ 1.0F
+ );
+ float torShape = Mth.lerp(Mth.clamp(widthWorld / 62.5F, 0.0F, 1.0F), tornado.shape(), 20.0F);
+ float funnelRadiusWorld = (widthWorld / 2.5F)
+ + ((widthWorld / 2.5F) * percFunnelHeight * tornado.touchdownProgress())
+ + ((stormSizeWorld / Math.max(Mth.lerp(tornado.touchdownProgress(), torShape + 2.0F, torShape), 0.001F))
+ * percFunnelHeight * percFunnelHeight * percFunnelHeight * percFunnelHeight);
+ return Mth.lerp((1.0F - percFunnelHeight) * (1.0F - tornado.touchdownProgress()), funnelRadiusWorld, 0.0F);
+ }
+
+ private static String fmt(double value) {
+ return String.format(Locale.ROOT, "%.3f", value);
+ }
+
+ private static boolean shouldDebugLog(ClientLevel level) {
+ return TornadoRenderDebugState.isActive() && level != null && level.getGameTime() % 20L == 0L;
+ }
+
+ private static float sampleTerrainSurfaceY(ClientLevel level, Vec3 renderPos, float radius) {
+ int centerX = Mth.floor(renderPos.x);
+ int centerZ = Mth.floor(renderPos.z);
+ int sampleOffset = Math.max(2, Mth.floor(Math.min(radius * 0.45F, 10.0F)));
+
+ float highestSurface = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX, centerZ) - 1.0F;
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX + sampleOffset, centerZ) - 1.0F);
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX - sampleOffset, centerZ) - 1.0F);
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX, centerZ + sampleOffset) - 1.0F);
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX, centerZ - sampleOffset) - 1.0F);
+ return highestSurface;
+ }
+
+ private static void debug(String message, Object... args) {
+ if (TornadoRenderDebugState.isActive()) {
+ ProjectAtmosphere.LOGGER.info("[TornadoDebug] " + message, args);
+ }
+ }
+
+ private record PreparedTornado(UUID id, float centerX, float centerZ, float bottomY, float height,
+ float width, float stormSize, float spin, float intensity,
+ float shape, float touchdownProgress, float seed, float animationTime,
+ Vec3 renderPosWorld, float renderBottomWorld, float terrainSurfaceWorld,
+ float bottomWorld, float topWorld, float cloudHeightWorld, float scale,
+ float boundsRadiusCloud, float boundsRadiusWorld, float wallcloudRadiusWorld) {
+ static PreparedTornado from(ClientLevel level, TornadoInstance tornado, float animationTime, float partialTick) {
+ float scale = SimpleCloudsConstants.CLOUD_SCALE;
+ float cloudHeight = CloudManager.get(level).getCloudHeight();
+ Vec3 renderPos = tornado.getRenderPosition(partialTick);
+ float renderBottomY = tornado.getRenderBottomY(partialTick);
+ float renderRadius = tornado.getRenderRadius(partialTick);
+ float terrainSurfaceY = sampleTerrainSurfaceY(level, renderPos, renderRadius);
+ float contactExtension = Math.max(GROUND_CONTACT_EXTENSION_WORLD, renderBottomY - terrainSurfaceY + GROUND_CONTACT_PADDING_WORLD);
+ float centerX = (float) renderPos.x / scale;
+ float centerZ = (float) renderPos.z / scale;
+ float bottomWorld = renderBottomY - contactExtension;
+ float topWorld = Math.max(
+ renderBottomY + tornado.getRenderHeight(partialTick),
+ cloudHeight + CLOUD_BLEND_PAD_ABOVE_CLOUD_BASE_WORLD
+ );
+ float bottomY = (bottomWorld - cloudHeight) / scale;
+ float height = Math.max((topWorld - bottomWorld) / scale, MIN_VISUAL_WORLD_HEIGHT / scale);
+ float width = Math.max(renderRadius * 2.0F, MIN_VISUAL_WORLD_WIDTH) / scale;
+ float stormSize = Math.max(MIN_VISUAL_WORLD_STORM_SIZE / scale, Math.max(width * 4.25F, height * 0.34F));
+ float boundsRadiusCloud = Math.max(width * 5.4F, stormSize * 0.58F);
+ float boundsRadiusWorld = boundsRadiusCloud * scale;
+ float wallcloudRadiusWorld = stormSize * scale * 0.35F;
+ float intensity = Mth.clamp(tornado.getNormalizedIntensity(), 0.0F, 1.0F);
+ float touchdownProgress = switch (tornado.getPhase()) {
+ case FORMING -> Mth.clamp(intensity * 1.35F, 0.0F, 0.92F);
+ case ACTIVE -> Mth.clamp(0.72F + intensity * 0.35F, 0.0F, 1.0F);
+ case DISSIPATING -> Mth.clamp(intensity * 1.10F, 0.0F, 1.0F);
+ default -> 0.0F;
+ };
+ float seed = (Math.abs(tornado.getId().hashCode()) % 10000) / 10000.0F;
+ float shape = 8.0F + seed * 10.0F;
+ return new PreparedTornado(
+ tornado.getId(),
+ centerX,
+ centerZ,
+ bottomY,
+ height,
+ width,
+ stormSize,
+ tornado.getVisualSpin(partialTick),
+ intensity,
+ shape,
+ touchdownProgress,
+ seed,
+ animationTime,
+ renderPos,
+ renderBottomY,
+ terrainSurfaceY,
+ bottomWorld,
+ topWorld,
+ cloudHeight,
+ scale,
+ boundsRadiusCloud,
+ boundsRadiusWorld,
+ wallcloudRadiusWorld
+ );
+ }
+
+ float heightWorld() {
+ return this.height * this.scale;
+ }
+
+ float widthWorld() {
+ return this.width * this.scale;
+ }
+
+ float stormSizeWorld() {
+ return this.stormSize * this.scale;
+ }
+
+ Vec3 originCloud() {
+ return new Vec3(this.centerX, this.bottomY, this.centerZ);
+ }
+
+ Vec3 boundsMinCloud() {
+ return new Vec3(
+ this.centerX - this.boundsRadiusCloud,
+ this.bottomY - (8.0F / this.scale),
+ this.centerZ - this.boundsRadiusCloud
+ );
+ }
+
+ Vec3 boundsMaxCloud() {
+ return new Vec3(
+ this.centerX + this.boundsRadiusCloud,
+ this.bottomY + this.height + (12.0F / this.scale),
+ this.centerZ + this.boundsRadiusCloud
+ );
+ }
+
+ Vec3 originWorld() {
+ return new Vec3(this.renderPosWorld.x, this.bottomWorld, this.renderPosWorld.z);
+ }
+
+ Vec3 centerWorld() {
+ return new Vec3(this.renderPosWorld.x, (this.bottomWorld + this.topWorld) * 0.5F, this.renderPosWorld.z);
+ }
+ }
+
+ private record AabbHit(float near, float far) {
+ }
+
+ private record CenterRayDiagnostic(Vec3 rayEndCloud, Vec3 rayDirectionCloud, float tNear, float tFar,
+ float stepSize, Vec3 samplePosCloud, Vec3 samplePosWorld,
+ Vec3 tornadoOriginCloud, Vec3 tornadoOriginWorld,
+ Vec3 localPosCloud, Vec3 localPosWorld,
+ DeterministicFunnelSample funnelSample) {
+ }
+
+ private record DeterministicFunnelSample(float height01, float heightMask, float radialMask,
+ float funnelRadiusWorld, float density, float alpha,
+ float wallcloudRadiusWorld, float wallcloudLowerWorld,
+ float connectionRadiusWorld) {
+ float radialDistanceWorld() {
+ return this.radialMask >= 1.0F ? 0.0F : this.funnelRadiusWorld * (1.0F - this.radialMask);
+ }
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/render/TornadoRenderDebugState.java b/src/main/java/net/Gabou/projectatmosphere/client/render/TornadoRenderDebugState.java
new file mode 100644
index 00000000..119c118c
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/client/render/TornadoRenderDebugState.java
@@ -0,0 +1,118 @@
+package net.Gabou.projectatmosphere.client.render;
+
+import net.Gabou.projectatmosphere.ProjectAtmosphere;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+public final class TornadoRenderDebugState {
+ public enum Mode {
+ OFF("off", 0),
+ BOX("box", 1),
+ HIT("hit", 2),
+ FILL("fill", 3),
+ FUNNEL("funnel", 4),
+ HEIGHT("height", 5),
+ RADIAL("radial", 6),
+ RADIUS("radius", 7),
+ DENSITY("density", 8),
+ ALPHA("alpha", 9),
+ WALLCLOUD("wallcloud", 10),
+ CONNECTION("connection", 11),
+ FULL("full", 12);
+
+ private final String token;
+ private final int shaderValue;
+
+ Mode(String token, int shaderValue) {
+ this.token = token;
+ this.shaderValue = shaderValue;
+ }
+
+ public String token() {
+ return this.token;
+ }
+
+ public int shaderValue() {
+ return this.shaderValue;
+ }
+
+ public static Mode fromToken(String token) {
+ if (token == null) {
+ return OFF;
+ }
+ String normalized = token.trim().toLowerCase(Locale.ROOT);
+ if (normalized.equals("aabb")) {
+ return BOX;
+ }
+ for (Mode mode : values()) {
+ if (mode.token.equals(normalized)) {
+ return mode;
+ }
+ }
+ return OFF;
+ }
+ }
+
+ private static Mode mode = Mode.OFF;
+ private static boolean freeze;
+ private static int requestedStormIndex = -1;
+ private static boolean diagnosticReportRequested;
+
+ private TornadoRenderDebugState() {
+ }
+
+ public static synchronized Mode getMode() {
+ return mode;
+ }
+
+ public static synchronized void setMode(Mode newMode) {
+ mode = newMode == null ? Mode.OFF : newMode;
+ }
+
+ public static synchronized boolean isFreezeEnabled() {
+ return freeze;
+ }
+
+ public static synchronized void setFreezeEnabled(boolean enabled) {
+ freeze = enabled;
+ }
+
+ public static synchronized int getRequestedStormIndex() {
+ return requestedStormIndex;
+ }
+
+ public static synchronized void setRequestedStormIndex(int index) {
+ requestedStormIndex = Math.max(-1, index);
+ }
+
+ public static synchronized boolean isActive() {
+ return ProjectAtmosphere.DEBUG_MODE && mode != Mode.OFF;
+ }
+
+ public static synchronized boolean isCommandAvailable() {
+ return ProjectAtmosphere.DEBUG_MODE;
+ }
+
+ public static synchronized void requestDiagnosticReport() {
+ diagnosticReportRequested = true;
+ }
+
+ public static synchronized boolean consumeDiagnosticReportRequest() {
+ boolean requested = diagnosticReportRequested;
+ diagnosticReportRequested = false;
+ return requested;
+ }
+
+ public static String supportedModes() {
+ return Arrays.stream(Mode.values())
+ .map(Mode::token)
+ .collect(Collectors.joining(", "));
+ }
+
+ public static synchronized String describe() {
+ String storm = requestedStormIndex < 0 ? "auto" : Integer.toString(requestedStormIndex);
+ return "mode=" + mode.token() + ", freeze=" + freeze + ", storm=" + storm;
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/render/TornadoShaders.java b/src/main/java/net/Gabou/projectatmosphere/client/render/TornadoShaders.java
new file mode 100644
index 00000000..77e9ed13
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/client/render/TornadoShaders.java
@@ -0,0 +1,40 @@
+package net.Gabou.projectatmosphere.client.render;
+
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import net.Gabou.projectatmosphere.ProjectAtmosphere;
+import net.minecraft.client.renderer.ShaderInstance;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.client.event.RegisterShadersEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+
+import java.io.IOException;
+
+@Mod.EventBusSubscriber(modid = ProjectAtmosphere.MODID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
+public final class TornadoShaders {
+ public static final ResourceLocation TORNADO_TEXTURE = ResourceLocation.fromNamespaceAndPath(ProjectAtmosphere.MODID, "textures/effects/tornado.png");
+ public static final ResourceLocation BASE_TEXTURE = ResourceLocation.fromNamespaceAndPath(ProjectAtmosphere.MODID, "textures/effects/base.png");
+ public static final ResourceLocation NOISE_TEXTURE = ResourceLocation.fromNamespaceAndPath(ProjectAtmosphere.MODID, "textures/effects/noise.png");
+ public static final ResourceLocation FLOW_TEXTURE = ResourceLocation.fromNamespaceAndPath(ProjectAtmosphere.MODID, "textures/effects/flowmap.png");
+
+ private static final ResourceLocation SHADER_ID = ResourceLocation.fromNamespaceAndPath(ProjectAtmosphere.MODID, "tornado_round");
+
+ private static ShaderInstance shader;
+
+ private TornadoShaders() {
+ }
+
+ @SubscribeEvent
+ public static void onRegisterShaders(RegisterShadersEvent event) throws IOException {
+ event.registerShader(new ShaderInstance(event.getResourceProvider(), SHADER_ID, DefaultVertexFormat.POSITION_TEX), loaded -> shader = loaded);
+ }
+
+ public static ShaderInstance getShader() {
+ return shader;
+ }
+
+ public static boolean isReady() {
+ return shader != null;
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/render/VolumeBoxMesh.java b/src/main/java/net/Gabou/projectatmosphere/client/render/VolumeBoxMesh.java
new file mode 100644
index 00000000..ee06bed2
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/client/render/VolumeBoxMesh.java
@@ -0,0 +1,82 @@
+package net.Gabou.projectatmosphere.client.render;
+
+import com.mojang.blaze3d.vertex.BufferBuilder;
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import com.mojang.blaze3d.vertex.Tesselator;
+import com.mojang.blaze3d.vertex.VertexBuffer;
+import com.mojang.blaze3d.vertex.VertexFormat;
+import net.minecraft.client.renderer.ShaderInstance;
+import org.joml.Matrix4f;
+
+public final class VolumeBoxMesh implements AutoCloseable {
+ private VertexBuffer buffer;
+
+ public void ensureInitialized() {
+ if (this.buffer != null) {
+ return;
+ }
+
+ BufferBuilder builder = Tesselator.getInstance().getBuilder();
+ builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX);
+
+ // Front (+Z)
+ vertex(builder, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F);
+ vertex(builder, 1.0F, 0.0F, 1.0F, 1.0F, 0.0F);
+ vertex(builder, 1.0F, 1.0F, 1.0F, 1.0F, 1.0F);
+ vertex(builder, 0.0F, 1.0F, 1.0F, 0.0F, 1.0F);
+
+ // Back (-Z)
+ vertex(builder, 1.0F, 0.0F, 0.0F, 0.0F, 0.0F);
+ vertex(builder, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F);
+ vertex(builder, 0.0F, 1.0F, 0.0F, 1.0F, 1.0F);
+ vertex(builder, 1.0F, 1.0F, 0.0F, 0.0F, 1.0F);
+
+ // Left (-X)
+ vertex(builder, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F);
+ vertex(builder, 0.0F, 0.0F, 1.0F, 1.0F, 0.0F);
+ vertex(builder, 0.0F, 1.0F, 1.0F, 1.0F, 1.0F);
+ vertex(builder, 0.0F, 1.0F, 0.0F, 0.0F, 1.0F);
+
+ // Right (+X)
+ vertex(builder, 1.0F, 0.0F, 1.0F, 0.0F, 0.0F);
+ vertex(builder, 1.0F, 0.0F, 0.0F, 1.0F, 0.0F);
+ vertex(builder, 1.0F, 1.0F, 0.0F, 1.0F, 1.0F);
+ vertex(builder, 1.0F, 1.0F, 1.0F, 0.0F, 1.0F);
+
+ // Top (+Y)
+ vertex(builder, 0.0F, 1.0F, 1.0F, 0.0F, 0.0F);
+ vertex(builder, 1.0F, 1.0F, 1.0F, 1.0F, 0.0F);
+ vertex(builder, 1.0F, 1.0F, 0.0F, 1.0F, 1.0F);
+ vertex(builder, 0.0F, 1.0F, 0.0F, 0.0F, 1.0F);
+
+ // Bottom (-Y)
+ vertex(builder, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F);
+ vertex(builder, 1.0F, 0.0F, 0.0F, 1.0F, 0.0F);
+ vertex(builder, 1.0F, 0.0F, 1.0F, 1.0F, 1.0F);
+ vertex(builder, 0.0F, 0.0F, 1.0F, 0.0F, 1.0F);
+
+ this.buffer = new VertexBuffer(VertexBuffer.Usage.STATIC);
+ this.buffer.bind();
+ this.buffer.upload(builder.end());
+ VertexBuffer.unbind();
+ }
+
+ public void draw(ShaderInstance shader, Matrix4f modelViewMat, Matrix4f projMat) {
+ this.ensureInitialized();
+ this.buffer.bind();
+ this.buffer.drawWithShader(modelViewMat, projMat, shader);
+ VertexBuffer.unbind();
+ }
+
+ @Override
+ public void close() {
+ if (this.buffer != null) {
+ this.buffer.close();
+ this.buffer = null;
+ }
+ }
+
+ private static void vertex(BufferBuilder builder, float x, float y, float z, float u, float v) {
+ builder.vertex(x, y, z).uv(u, v).endVertex();
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/client/screen/WeatherRadarScreen.java b/src/main/java/net/Gabou/projectatmosphere/client/screen/WeatherRadarScreen.java
index 12757fec..cd8cea5a 100644
--- a/src/main/java/net/Gabou/projectatmosphere/client/screen/WeatherRadarScreen.java
+++ b/src/main/java/net/Gabou/projectatmosphere/client/screen/WeatherRadarScreen.java
@@ -97,7 +97,7 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia
}
// Overlay: Tornadoes (purple) and Hurricanes (black)
- for (TornadoInstance t : TornadoManager.getActiveTornadoes()) {
+ for (TornadoInstance t : TornadoManager.getClientTornadoes()) {
float dx = (float) ((t.position.x - player.getX()) / scale);
float dz = (float) ((t.position.z - player.getZ()) / scale);
int r = Math.max(2, Math.round(t.radius / scale));
diff --git a/src/main/java/net/Gabou/projectatmosphere/mixin/CloudMeshGeneratorDiagnosticsAccessor.java b/src/main/java/net/Gabou/projectatmosphere/mixin/CloudMeshGeneratorDiagnosticsAccessor.java
new file mode 100644
index 00000000..0da22f81
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/mixin/CloudMeshGeneratorDiagnosticsAccessor.java
@@ -0,0 +1,24 @@
+package net.Gabou.projectatmosphere.mixin;
+
+import dev.nonamecrackers2.simpleclouds.client.mesh.chunk.MeshChunk;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.CloudMeshGenerator;
+import org.apache.commons.lang3.tuple.Pair;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import java.util.List;
+
+@Mixin(value = CloudMeshGenerator.class, remap = false)
+public interface CloudMeshGeneratorDiagnosticsAccessor {
+ @Accessor("chunks")
+ List projectatmosphere$getChunks();
+
+ @Accessor("opaqueBufferSize")
+ int projectatmosphere$getOpaqueBufferSize();
+
+ @Accessor("transparentBufferSize")
+ int projectatmosphere$getTransparentBufferSize();
+
+ @Accessor("meshGenStatus")
+ Pair projectatmosphere$getMeshGenStatus();
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/mixin/MultiRegionCloudMeshGeneratorMixin.java b/src/main/java/net/Gabou/projectatmosphere/mixin/MultiRegionCloudMeshGeneratorMixin.java
index fa83d463..4029cb8c 100644
--- a/src/main/java/net/Gabou/projectatmosphere/mixin/MultiRegionCloudMeshGeneratorMixin.java
+++ b/src/main/java/net/Gabou/projectatmosphere/mixin/MultiRegionCloudMeshGeneratorMixin.java
@@ -33,6 +33,7 @@
import net.Gabou.projectatmosphere.util.HurricaneUpload;
import net.Gabou.projectatmosphere.util.RegionUpload;
import net.Gabou.projectatmosphere.util.TornadoUpload;
+import org.apache.commons.lang3.ArrayUtils;
import java.io.IOException;
@@ -251,6 +252,7 @@ public abstract class MultiRegionCloudMeshGeneratorMixin {
this.projectatmosphere$tornadoBuffer = null;
}
if (this.regionTextureGenerator == null || this.projectatmosphere$tornadoBuffer != null) {
+ this.projectatmosphere$bindStormBufferToMeshShader(this.projectatmosphere$tornadoBuffer, PROJECTATMOSPHERE$TORNADO_BUFFER_NAME);
return;
}
if (!this.projectatmosphere$supportsTornadoBuffer()) {
@@ -260,14 +262,15 @@ public abstract class MultiRegionCloudMeshGeneratorMixin {
if (this.projectatmosphere$tornadoBuffer != null) {
this.projectatmosphere$tornadoBuffer.allocateBuffer(PROJECTATMOSPHERE$MAX_TORNADOES * PROJECTATMOSPHERE$TORNADO_STRIDE);
}
+ this.projectatmosphere$bindStormBufferToMeshShader(this.projectatmosphere$tornadoBuffer, PROJECTATMOSPHERE$TORNADO_BUFFER_NAME);
}
-
@Unique
private void projectatmosphere$ensureHurricaneBuffer() {
if (this.projectatmosphere$hurricaneBuffer != null && this.projectatmosphere$hurricaneBuffer.getId() == -1) {
this.projectatmosphere$hurricaneBuffer = null;
}
if (this.regionTextureGenerator == null || this.projectatmosphere$hurricaneBuffer != null) {
+ this.projectatmosphere$bindStormBufferToMeshShader(this.projectatmosphere$hurricaneBuffer, PROJECTATMOSPHERE$HURRICANE_BUFFER_NAME);
return;
}
if (!this.projectatmosphere$supportsHurricaneBuffer()) {
@@ -277,8 +280,10 @@ public abstract class MultiRegionCloudMeshGeneratorMixin {
if (this.projectatmosphere$hurricaneBuffer != null) {
this.projectatmosphere$hurricaneBuffer.allocateBuffer(PROJECTATMOSPHERE$MAX_HURRICANES * PROJECTATMOSPHERE$HURRICANE_STRIDE);
}
+ this.projectatmosphere$bindStormBufferToMeshShader(this.projectatmosphere$hurricaneBuffer, PROJECTATMOSPHERE$HURRICANE_BUFFER_NAME);
}
+
@Unique
private List projectatmosphere$collectTornadoUploads(float partialTick) {
List uploads = new ArrayList<>();
@@ -352,9 +357,11 @@ public abstract class MultiRegionCloudMeshGeneratorMixin {
if (this.cloudGetter == null) {
return regions;
}
+ int rejectedRegions = 0;
for (CloudRegion region : this.cloudGetter.getClouds()) {
float[] upload = this.projectatmosphere$buildRegionUpload(partialTick, region);
if (!this.projectatmosphere$isRegionValid(upload)) {
+ rejectedRegions++;
continue;
}
regions.add(new RegionUpload(region, upload));
@@ -362,6 +369,15 @@ public abstract class MultiRegionCloudMeshGeneratorMixin {
break;
}
}
+ if (PROJECTATMOSPHERE$LOGGER.isDebugEnabled()) {
+ PROJECTATMOSPHERE$LOGGER.debug(
+ "Renderable cloud regions={} rejected={} cachedTypes={} cloudGetter={}",
+ regions.size(),
+ rejectedRegions,
+ this.cachedTypes == null ? 0 : this.cachedTypes.length,
+ this.cloudGetter
+ );
+ }
return regions;
}
@@ -395,11 +411,24 @@ public abstract class MultiRegionCloudMeshGeneratorMixin {
if (type == null) {
return -1;
}
+ int index = ArrayUtils.indexOf(this.cachedTypes, type);
+ if (index >= 0) {
+ return index;
+ }
for (int i = 0; i < this.cachedTypes.length; i++) {
- if (Objects.equals(this.cachedTypes[i], type)) {
+ CloudInfo cachedType = this.cachedTypes[i];
+ if (cachedType instanceof CloudType cachedCloudType && Objects.equals(cachedCloudType.id(), type.id())) {
return i;
}
}
+ if (PROJECTATMOSPHERE$LOGGER.isDebugEnabled()) {
+ PROJECTATMOSPHERE$LOGGER.debug(
+ "Cloud type '{}' was not found in the cached Simple Clouds type table; cachedTypes={} getterType={}",
+ cloudTypeId,
+ this.cachedTypes.length,
+ type
+ );
+ }
return -1;
}
@@ -474,11 +503,21 @@ public abstract class MultiRegionCloudMeshGeneratorMixin {
}
}
+ @Unique
+ private void projectatmosphere$bindStormBufferToMeshShader(ShaderStorageBufferObject buffer, String name) {
+ ComputeShader shader = this.projectatmosphere$getShader();
+ if (buffer == null || buffer.getId() == -1 || shader == null || !shader.isValid() || shader.getId() <= 0) {
+ return;
+ }
+ buffer.optionalBindToProgram(name, shader.getId());
+ }
+
@Unique
private static Method projectatmosphere$skipSettingsMethod;
@Unique
private static Method projectatmosphere$heightSettingsMethod;
+
@Unique
private ComputeShader projectatmosphere$getShader() {
return ((CloudMeshGeneratorAccessor)(Object)this).projectatmosphere$getShader();
diff --git a/src/main/java/net/Gabou/projectatmosphere/mixin/client/DefaultPipelineTornadoMixin.java b/src/main/java/net/Gabou/projectatmosphere/mixin/client/DefaultPipelineTornadoMixin.java
new file mode 100644
index 00000000..485f60c0
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/mixin/client/DefaultPipelineTornadoMixin.java
@@ -0,0 +1,69 @@
+package net.Gabou.projectatmosphere.mixin.client;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import dev.nonamecrackers2.simpleclouds.client.renderer.SimpleCloudsRenderer;
+import dev.nonamecrackers2.simpleclouds.client.renderer.pipeline.DefaultPipeline;
+import net.Gabou.projectatmosphere.client.render.SimpleCloudsTornadoRenderer;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.renderer.culling.Frustum;
+import org.joml.Matrix4f;
+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(value = DefaultPipeline.class, remap = false)
+public abstract class DefaultPipelineTornadoMixin {
+ @Inject(
+ method = "afterSky",
+ at = @At(
+ value = "INVOKE",
+ target = "Ldev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer;copyDepthFromCloudsToMain()V"
+ )
+ )
+ private void projectatmosphere$renderTornadoOpaque(Minecraft mc, SimpleCloudsRenderer renderer,
+ PoseStack stack, Matrix4f projMat, float partialTick,
+ double camX, double camY, double camZ, Frustum frustum,
+ CallbackInfo ci) {
+ ClientLevel level = mc.level;
+ if (level == null) {
+ return;
+ }
+ float[] cloudColor = renderer.getCloudColor(partialTick);
+ mc.getProfiler().push("projectatmosphere_tornado_opaque");
+ SimpleCloudsTornadoRenderer.INSTANCE.prepareFrame(level, partialTick);
+ renderer.getCloudTarget().bindWrite(false);
+ SimpleCloudsTornadoRenderer.INSTANCE.renderOpaque(
+ renderer, stack, projMat, partialTick, cloudColor[0], cloudColor[1], cloudColor[2]
+ );
+ mc.getProfiler().pop();
+ }
+
+ @Inject(
+ method = "afterSky",
+ at = @At(
+ value = "INVOKE",
+ target = "Lcom/mojang/blaze3d/vertex/PoseStack;popPose()V",
+ shift = At.Shift.BEFORE
+ )
+ )
+ private void projectatmosphere$renderTornadoTransparency(Minecraft mc, SimpleCloudsRenderer renderer,
+ PoseStack stack, Matrix4f projMat, float partialTick,
+ double camX, double camY, double camZ, Frustum frustum,
+ CallbackInfo ci) {
+ ClientLevel level = mc.level;
+ if (level == null) {
+ return;
+ }
+ float[] cloudColor = renderer.getCloudColor(partialTick);
+ mc.getProfiler().push("projectatmosphere_tornado_transparency");
+ SimpleCloudsTornadoRenderer.INSTANCE.prepareFrame(level, partialTick);
+ renderer.copyDepthFromCloudsToTransparency();
+ renderer.getCloudTransparencyTarget().bindWrite(false);
+ SimpleCloudsTornadoRenderer.INSTANCE.renderTransparency(
+ renderer, stack, projMat, partialTick, cloudColor[0], cloudColor[1], cloudColor[2]
+ );
+ mc.getProfiler().pop();
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/mixin/client/InstanceableMeshDiagnosticsMixin.java b/src/main/java/net/Gabou/projectatmosphere/mixin/client/InstanceableMeshDiagnosticsMixin.java
new file mode 100644
index 00000000..08f717df
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/mixin/client/InstanceableMeshDiagnosticsMixin.java
@@ -0,0 +1,16 @@
+package net.Gabou.projectatmosphere.mixin.client;
+
+import dev.nonamecrackers2.simpleclouds.client.mesh.instancing.InstanceableMesh;
+import net.Gabou.projectatmosphere.client.render.SimpleCloudsRenderDiagnostics;
+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(value = InstanceableMesh.class, remap = false)
+public abstract class InstanceableMeshDiagnosticsMixin {
+ @Inject(method = "drawInstanced", at = @At("HEAD"))
+ private void projectatmosphere$recordDrawCount(int count, CallbackInfo ci) {
+ SimpleCloudsRenderDiagnostics.recordDraw("simpleclouds", count);
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/mixin/client/ShaderSupportPipelineTornadoMixin.java b/src/main/java/net/Gabou/projectatmosphere/mixin/client/ShaderSupportPipelineTornadoMixin.java
new file mode 100644
index 00000000..13ab2bba
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/mixin/client/ShaderSupportPipelineTornadoMixin.java
@@ -0,0 +1,70 @@
+package net.Gabou.projectatmosphere.mixin.client;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import dev.nonamecrackers2.simpleclouds.client.renderer.SimpleCloudsRenderer;
+import dev.nonamecrackers2.simpleclouds.client.renderer.pipeline.ShaderSupportPipeline;
+import net.Gabou.projectatmosphere.client.render.SimpleCloudsTornadoRenderer;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.renderer.culling.Frustum;
+import org.joml.Matrix4f;
+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(value = ShaderSupportPipeline.class, remap = false)
+public abstract class ShaderSupportPipelineTornadoMixin {
+ @Inject(
+ method = "afterLevel",
+ at = @At(
+ value = "INVOKE",
+ target = "Ldev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer;getCloudTransparencyTarget()Ldev/nonamecrackers2/simpleclouds/client/framebuffer/WeightedBlendingTarget;"
+ )
+ )
+ private void projectatmosphere$renderTornadoOpaque(Minecraft mc, SimpleCloudsRenderer renderer,
+ PoseStack stack, Matrix4f projMat, float partialTick,
+ double camX, double camY, double camZ, Frustum frustum,
+ CallbackInfo ci) {
+ ClientLevel level = mc.level;
+ if (level == null) {
+ return;
+ }
+ float[] cloudColor = renderer.getCloudColor(partialTick);
+ mc.getProfiler().push("projectatmosphere_tornado_opaque");
+ SimpleCloudsTornadoRenderer.INSTANCE.prepareFrame(level, partialTick);
+ renderer.getCloudTarget().bindWrite(false);
+ SimpleCloudsTornadoRenderer.INSTANCE.renderOpaque(
+ renderer, stack, projMat, partialTick, cloudColor[0], cloudColor[1], cloudColor[2],
+ renderer.getCloudTarget().getDepthTextureId(), true
+ );
+ mc.getProfiler().pop();
+ }
+
+ @Inject(
+ method = "afterLevel",
+ at = @At(
+ value = "INVOKE",
+ target = "Lcom/mojang/blaze3d/vertex/PoseStack;popPose()V",
+ shift = At.Shift.BEFORE
+ )
+ )
+ private void projectatmosphere$renderTornadoTransparency(Minecraft mc, SimpleCloudsRenderer renderer,
+ PoseStack stack, Matrix4f projMat, float partialTick,
+ double camX, double camY, double camZ, Frustum frustum,
+ CallbackInfo ci) {
+ ClientLevel level = mc.level;
+ if (level == null) {
+ return;
+ }
+ float[] cloudColor = renderer.getCloudColor(partialTick);
+ mc.getProfiler().push("projectatmosphere_tornado_transparency");
+ SimpleCloudsTornadoRenderer.INSTANCE.prepareFrame(level, partialTick);
+ renderer.copyDepthFromCloudsToTransparency();
+ renderer.getCloudTransparencyTarget().bindWrite(false);
+ SimpleCloudsTornadoRenderer.INSTANCE.renderTransparency(
+ renderer, stack, projMat, partialTick, cloudColor[0], cloudColor[1], cloudColor[2]
+ );
+ mc.getProfiler().pop();
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/mixin/client/SimpleCloudsRendererDiagnosticsMixin.java b/src/main/java/net/Gabou/projectatmosphere/mixin/client/SimpleCloudsRendererDiagnosticsMixin.java
new file mode 100644
index 00000000..d8d37245
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/mixin/client/SimpleCloudsRendererDiagnosticsMixin.java
@@ -0,0 +1,58 @@
+package net.Gabou.projectatmosphere.mixin.client;
+
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.CloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.renderer.SimpleCloudsRenderer;
+import net.Gabou.projectatmosphere.client.render.SimpleCloudsRenderDiagnostics;
+import net.Gabou.projectatmosphere.mixin.CloudMeshGeneratorDiagnosticsAccessor;
+import net.minecraft.client.renderer.culling.Frustum;
+import org.joml.Matrix4f;
+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(value = SimpleCloudsRenderer.class, remap = false)
+public abstract class SimpleCloudsRendererDiagnosticsMixin {
+ @Inject(
+ method = "renderCloudsOpaque(Ldev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator;Lcom/mojang/blaze3d/vertex/PoseStack;Lorg/joml/Matrix4f;FFFFFFLnet/minecraft/client/renderer/culling/Frustum;Z)V",
+ at = @At("HEAD")
+ )
+ private static void projectatmosphere$beginOpaquePass(CloudMeshGenerator generator, com.mojang.blaze3d.vertex.PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, Frustum frustum, boolean ditherFade, CallbackInfo ci) {
+ if (generator == null) {
+ return;
+ }
+ CloudMeshGeneratorDiagnosticsAccessor accessor = (CloudMeshGeneratorDiagnosticsAccessor)(Object)generator;
+ SimpleCloudsRenderDiagnostics.beginPass(
+ "opaque",
+ accessor.projectatmosphere$getChunks() == null ? 0 : accessor.projectatmosphere$getChunks().size(),
+ accessor.projectatmosphere$getOpaqueBufferSize(),
+ accessor.projectatmosphere$getTransparentBufferSize(),
+ accessor.projectatmosphere$getMeshGenStatus()
+ );
+ }
+
+ @Inject(method = "renderCloudsOpaque(Ldev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator;Lcom/mojang/blaze3d/vertex/PoseStack;Lorg/joml/Matrix4f;FFFFFFLnet/minecraft/client/renderer/culling/Frustum;Z)V", at = @At("RETURN"))
+ private static void projectatmosphere$endOpaquePass(CloudMeshGenerator generator, com.mojang.blaze3d.vertex.PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, Frustum frustum, boolean ditherFade, CallbackInfo ci) {
+ SimpleCloudsRenderDiagnostics.endPass();
+ }
+
+ @Inject(method = "renderCloudsTransparency(Ldev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator;Lcom/mojang/blaze3d/vertex/PoseStack;Lorg/joml/Matrix4f;FFFFFFLnet/minecraft/client/renderer/culling/Frustum;Z)V", at = @At("HEAD"))
+ private static void projectatmosphere$beginTransparencyPass(CloudMeshGenerator generator, com.mojang.blaze3d.vertex.PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, Frustum frustum, boolean ditherFade, CallbackInfo ci) {
+ if (generator == null) {
+ return;
+ }
+ CloudMeshGeneratorDiagnosticsAccessor accessor = (CloudMeshGeneratorDiagnosticsAccessor)(Object)generator;
+ SimpleCloudsRenderDiagnostics.beginPass(
+ "transparent",
+ accessor.projectatmosphere$getChunks() == null ? 0 : accessor.projectatmosphere$getChunks().size(),
+ accessor.projectatmosphere$getOpaqueBufferSize(),
+ accessor.projectatmosphere$getTransparentBufferSize(),
+ accessor.projectatmosphere$getMeshGenStatus()
+ );
+ }
+
+ @Inject(method = "renderCloudsTransparency(Ldev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator;Lcom/mojang/blaze3d/vertex/PoseStack;Lorg/joml/Matrix4f;FFFFFFLnet/minecraft/client/renderer/culling/Frustum;Z)V", at = @At("RETURN"))
+ private static void projectatmosphere$endTransparencyPass(CloudMeshGenerator generator, com.mojang.blaze3d.vertex.PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, Frustum frustum, boolean ditherFade, CallbackInfo ci) {
+ SimpleCloudsRenderDiagnostics.endPass();
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneInstance.java b/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneInstance.java
index 137e5a8f..753d8f54 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneInstance.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneInstance.java
@@ -1,5 +1,6 @@
package net.Gabou.projectatmosphere.modules.hurricane;
+import dev.nonamecrackers2.simpleclouds.common.world.CloudManager;
import net.Gabou.projectatmosphere.modules.atmosphere.CycloneSnapshot;
import net.Gabou.projectatmosphere.modules.core.WindVector;
import net.Gabou.projectatmosphere.modules.weather.StormShieldManager;
@@ -29,7 +30,9 @@ public class HurricaneInstance {
public static final ResourceLocation HURRICANE_CLOUD_TYPE_ID =
ResourceLocation.fromNamespaceAndPath("projectatmosphere", "hurricane");
- private static final float DEFAULT_ANCHOR_Y = 64.0F;
+ private static final float DEFAULT_ANCHOR_Y = 384.0F;
+ private static final float MIN_WORLD_ANCHOR_Y = 256.0F;
+ private static final float CLOUD_LAYER_DESCENT = 200.0F;
private static final int WIND_FIELD_INTERVAL_TICKS = 2;
private static final int DESTRUCTION_INTERVAL_TICKS = 8;
@@ -46,6 +49,7 @@ public class HurricaneInstance {
private float cycloneRadius;
private float cycloneIntensity;
private float destructiveStrength;
+ private float anchorY = DEFAULT_ANCHOR_Y;
private int ageTicks;
private long lastWindFieldTick = Long.MIN_VALUE;
private long lastDestructionTick = Long.MIN_VALUE;
@@ -77,6 +81,17 @@ public static HurricaneInstance fromCyclone(ServerLevel level, CycloneSnapshot s
return hurricane;
}
+ public void refreshAnchorY(Level level) {
+ CloudManager> manager = CloudManager.get(level);
+ if (manager == null) {
+ this.anchorY = Math.max(this.anchorY, MIN_WORLD_ANCHOR_Y);
+ return;
+ }
+
+ float cloudHeight = manager.getCloudHeight();
+ this.anchorY = Math.max(MIN_WORLD_ANCHOR_Y, cloudHeight - CLOUD_LAYER_DESCENT);
+ }
+
public void updateFromCyclone(ServerLevel level, CycloneSnapshot snapshot, WindVector ambientWind,
HurricaneCategory nextCategory, float intensificationStrength) {
this.position = new Vec3(snapshot.centerX(), level.getSeaLevel(), snapshot.centerZ());
@@ -93,6 +108,7 @@ public void updateFromCyclone(ServerLevel level, CycloneSnapshot snapshot, WindV
float boostedBase = Math.max(ambientWind.baseSpeed(), 11.0F + this.destructiveStrength * 22.0F);
float boostedGust = Math.max(ambientWind.gustSpeed(), boostedBase + 6.0F + this.category.ordinal() * 3.0F);
this.wind = new WindVector(boostedBase, ambientWind.angleRadians(), boostedGust);
+ this.refreshAnchorY(level);
}
public float getLifetimeSeconds() {
@@ -117,7 +133,7 @@ public UUID getCycloneId() {
}
public float getAnchorY() {
- return DEFAULT_ANCHOR_Y;
+ return this.anchorY;
}
public float getCoreRadius() {
@@ -199,6 +215,7 @@ public void tick(Level level) {
}
this.ageTicks++;
+ this.refreshAnchorY(level);
ServerLevel serverLevel = (ServerLevel) level;
long gameTime = serverLevel.getGameTime();
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneManager.java b/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneManager.java
index 08ee5695..a9824715 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneManager.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneManager.java
@@ -54,6 +54,7 @@ private HurricaneManager() {
public static void spawnServer(ServerLevel level, Vec3 pos, float radius, WindVector wind, HurricaneCategory category) {
HurricaneInstance hurricane = HurricaneInstance.createDebug(pos, radius, wind, category);
+ hurricane.refreshAnchorY(level);
DEBUG_HURRICANES.add(hurricane);
RESERVATION_REGIONS.put(hurricane.id, HurricaneSemantics.createReservationRegion(hurricane));
dirty = true;
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoCommand.java b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoCommand.java
index 09865dd8..1860597b 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoCommand.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoCommand.java
@@ -1,38 +1,65 @@
package net.Gabou.projectatmosphere.modules.tornado;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudRegion;
import dev.nonamecrackers2.simpleclouds.common.world.CloudManager;
import dev.nonamecrackers2.simpleclouds.common.world.SpawnRegion;
-import net.Gabou.projectatmosphere.ProjectAtmosphere;
import net.Gabou.projectatmosphere.api.WindVectorApi;
-import net.Gabou.projectatmosphere.api.common.cloud.region.ITornadoRegion;
-import net.Gabou.projectatmosphere.api.common.cloud.region.TornadoDescriptor;
import net.Gabou.projectatmosphere.compat.SimpleCloudsCompat;
import net.Gabou.projectatmosphere.util.AtmosphereUtils;
import net.Gabou.projectatmosphere.util.BiomeInstanceKey;
-import net.Gabou.projectatmosphere.util.DelayedTaskScheduler;
import net.Gabou.projectatmosphere.util.RegionInstanceKey;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
-import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.Mth;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
+import java.util.Comparator;
+
public class TornadoCommand {
- private static final ResourceLocation PROJECTATMOSPHERE$TORNADO_CONTROLLER =
- new ResourceLocation(ProjectAtmosphere.MODID, "command_spawn");
private static final String PROJECTATMOSPHERE$CUMULONIMBUS_ID = "simpleclouds:cumulonimbus";
private static final int PROJECTATMOSPHERE$SEARCH_RADIUS = 10;
private static final float PROJECTATMOSPHERE$DEFAULT_RADIUS = 10.0F;
- private static final int PROJECTATMOSPHERE$SPAWN_DELAY_TICKS = 500;
- private static final int PROJECTATMOSPHERE$AWAIT_INTERVAL = 100;
- private static final int PROJECTATMOSPHERE$AWAIT_POLLS = 40;
+ private static final double PROJECTATMOSPHERE$DEFAULT_REMOVE_RADIUS = 256.0D;
public static void appendTo(LiteralArgumentBuilder root) {
+ LiteralArgumentBuilder noCloudsBaseCommand = Commands.literal("spawnTornadoNoClouds")
+ .requires(source -> source.hasPermission(2))
+ .executes(ctx -> {
+ ServerPlayer player = ctx.getSource().getPlayerOrException();
+ ServerLevel level = player.serverLevel();
+ if (!level.dimension().equals(Level.OVERWORLD)) {
+ return 0;
+ }
+
+ Vec3 playerPos = player.position();
+ Vec3 tornadoPos = new Vec3(playerPos.x, level.getSeaLevel(), playerPos.z);
+ RegionInstanceKey regionKey = RegionInstanceKey.from(player.blockPosition());
+ WindVectorApi.WindSample sample = WindVectorApi.getOrFallback(regionKey, level.getGameTime());
+ net.Gabou.projectatmosphere.modules.core.WindVector wind =
+ net.Gabou.projectatmosphere.modules.core.WindVector.fromBase(
+ sample.speedMps(),
+ (float) Math.toRadians(sample.directionDeg())
+ );
+
+ if (TornadoManager.forceSpawnServerWithoutCloud(level, tornadoPos, PROJECTATMOSPHERE$DEFAULT_RADIUS, wind)) {
+ ctx.getSource().sendSuccess(
+ () -> Component.literal("Standalone tornado spawned without requiring clouds."),
+ true
+ );
+ return 1;
+ }
+
+ ctx.getSource().sendFailure(Component.literal("Unable to force-spawn standalone tornado."));
+ return 0;
+ });
+ root.then(noCloudsBaseCommand);
+
LiteralArgumentBuilder baseCommand = Commands.literal("spawnTornado")
.requires(source -> source.hasPermission(2))
.executes(ctx -> {
@@ -41,11 +68,13 @@ public static void appendTo(LiteralArgumentBuilder root) {
if (!level.dimension().equals(Level.OVERWORLD)) {
return 0;
}
+
Vec3 playerPos = player.position();
Vec3 tornadoPos = new Vec3(playerPos.x, level.getSeaLevel(), playerPos.z);
BiomeInstanceKey key = new BiomeInstanceKey(
AtmosphereUtils.getBiomeLocation(player.blockPosition(), level),
- player.blockPosition());
+ player.blockPosition()
+ );
RegionInstanceKey regionKey = RegionInstanceKey.from(player.blockPosition());
WindVectorApi.WindSample sample = WindVectorApi.getOrFallback(regionKey, level.getGameTime());
net.Gabou.projectatmosphere.modules.core.WindVector wind =
@@ -56,34 +85,52 @@ public static void appendTo(LiteralArgumentBuilder root) {
CloudRegion existing = projectatmosphere$findCumulonimbus(level, tornadoPos);
if (existing != null) {
- if (projectatmosphere$attachDescriptor(level, existing, tornadoPos)) {
+ if (TornadoManager.spawnServer(level, tornadoPos, PROJECTATMOSPHERE$DEFAULT_RADIUS, wind)) {
ctx.getSource().sendSuccess(
- () -> Component.literal("ðŸŒªï¸ Tornado engaged using SimpleClouds cumulonimbus."), true);
+ () -> Component.literal("Tornado engaged using SimpleClouds cumulonimbus."),
+ true
+ );
return 1;
}
- projectatmosphere$awaitCloud(ctx.getSource(), level, tornadoPos, PROJECTATMOSPHERE$AWAIT_POLLS);
- return 1;
+ if (TornadoManager.forceSpawnServerWithoutCloud(level, tornadoPos, PROJECTATMOSPHERE$DEFAULT_RADIUS, wind)) {
+ ctx.getSource().sendSuccess(
+ () -> Component.literal("Cloud attachment failed, so a standalone tornado was force-spawned."),
+ true
+ );
+ return 1;
+ }
+ ctx.getSource().sendFailure(Component.literal("Unable to spawn tornado."));
+ return 0;
}
CloudRegion spawnedRegion = SimpleCloudsCompat.spawnCloudInBiome(
- "cumulonimbus", key, level, null, wind);
+ "cumulonimbus",
+ key,
+ level,
+ null,
+ wind
+ );
if (spawnedRegion != null) {
- CommandSourceStack source = ctx.getSource();
- DelayedTaskScheduler.schedule(PROJECTATMOSPHERE$SPAWN_DELAY_TICKS, () -> {
- if (projectatmosphere$attachDescriptor(level, spawnedRegion, tornadoPos)) {
- source.sendSuccess(
- () -> Component.literal("ðŸŒªï¸ Tornado engaged once the seeded cumulonimbus matured."), true);
- } else {
- projectatmosphere$awaitCloud(source, level, tornadoPos, PROJECTATMOSPHERE$AWAIT_POLLS);
- }
- });
+ if (TornadoManager.forceSpawnServerWithoutCloud(level, tornadoPos, PROJECTATMOSPHERE$DEFAULT_RADIUS, wind)) {
+ ctx.getSource().sendSuccess(
+ () -> Component.literal("Seeded a cumulonimbus and force-spawned a tornado immediately."),
+ true
+ );
+ return 1;
+ }
+ ctx.getSource().sendFailure(Component.literal("Unable to spawn tornado after seeding a cumulonimbus."));
+ return 0;
+ }
+
+ if (TornadoManager.forceSpawnServerWithoutCloud(level, tornadoPos, PROJECTATMOSPHERE$DEFAULT_RADIUS, wind)) {
ctx.getSource().sendSuccess(
- () -> Component.literal("â˜ï¸ Seeded a cumulonimbus; waiting for SimpleClouds tornado engagement."), true);
+ () -> Component.literal("Forced a standalone tornado because no cumulonimbus could be attached."),
+ true
+ );
return 1;
}
-
- projectatmosphere$awaitCloud(ctx.getSource(), level, tornadoPos, PROJECTATMOSPHERE$AWAIT_POLLS);
- return 1;
+ ctx.getSource().sendFailure(Component.literal("Unable to spawn tornado."));
+ return 0;
});
root.then(baseCommand);
@@ -93,64 +140,68 @@ public static void appendTo(LiteralArgumentBuilder root) {
root.then(Commands.literal("cleartornadoes")
.requires(source -> source.hasPermission(2))
- .executes(ctx -> {
- ServerLevel level = ctx.getSource().getLevel();
- TornadoManager.clearTornadoes();
- ctx.getSource().sendSuccess(
- () -> Component.literal("ðŸŒªï¸ All tornadoes cleared."), true);
- return 1;
- }));
- root.then(Commands.literal("removetornado")
+ .executes(ctx -> projectatmosphere$clearAllTornadoes(ctx.getSource())));
+
+ LiteralArgumentBuilder removeTornado = Commands.literal("removetornado")
.requires(source -> source.hasPermission(2))
- .executes(ctx -> {
- ServerPlayer player = ctx.getSource().getPlayerOrException();
- ServerLevel level = player.serverLevel();
- if (!level.dimension().equals(Level.OVERWORLD)) {
- return 0;
- }
- Vec3 playerPos = player.position();
- TornadoInstance tornado = TornadoManager.getActiveTornadoes().stream()
- .filter(t -> t.position.distanceToSqr(playerPos) < 100)
- .findFirst()
- .orElse(null);
- if (tornado != null) {
- TornadoManager.removeTornado(tornado);
- ctx.getSource().sendSuccess(
- () -> Component.literal("ðŸŒªï¸ Tornado removed."), true);
- } else {
- ctx.getSource().sendFailure(
- Component.literal("No tornado found near you."));
- }
- return 1;
- }));
+ .executes(ctx -> projectatmosphere$removeNearestTornado(ctx.getSource(), PROJECTATMOSPHERE$DEFAULT_REMOVE_RADIUS))
+ .then(Commands.argument("radius", IntegerArgumentType.integer(1))
+ .executes(ctx -> projectatmosphere$removeNearestTornado(
+ ctx.getSource(),
+ IntegerArgumentType.getInteger(ctx, "radius")
+ )))
+ .then(Commands.literal("all")
+ .executes(ctx -> projectatmosphere$clearAllTornadoes(ctx.getSource())));
+ root.then(removeTornado);
+ root.then(Commands.literal("remove")
+ .requires(source -> source.hasPermission(2))
+ .then(Commands.literal("tornado")
+ .executes(ctx -> projectatmosphere$removeNearestTornado(ctx.getSource(), PROJECTATMOSPHERE$DEFAULT_REMOVE_RADIUS))
+ .then(Commands.argument("radius", IntegerArgumentType.integer(1))
+ .executes(ctx -> projectatmosphere$removeNearestTornado(
+ ctx.getSource(),
+ IntegerArgumentType.getInteger(ctx, "radius")
+ )))
+ .then(Commands.literal("all")
+ .executes(ctx -> projectatmosphere$clearAllTornadoes(ctx.getSource())))));
}
- private static void projectatmosphere$awaitCloud(CommandSourceStack source,
- ServerLevel level,
- Vec3 tornadoPos,
- int remainingPolls) {
- if (remainingPolls <= 0) {
- source.sendFailure(Component.literal("âš ï¸ No SimpleClouds cumulonimbus became available for this tornado."));
- return;
+ private static int projectatmosphere$removeNearestTornado(CommandSourceStack source, double maxDistance) throws com.mojang.brigadier.exceptions.CommandSyntaxException {
+ ServerPlayer player = source.getPlayerOrException();
+ ServerLevel level = player.serverLevel();
+ if (!level.dimension().equals(Level.OVERWORLD)) {
+ return 0;
}
- DelayedTaskScheduler.schedule(PROJECTATMOSPHERE$AWAIT_INTERVAL, () -> {
- CloudRegion region = projectatmosphere$findCumulonimbus(level, tornadoPos);
- if (region != null && projectatmosphere$attachDescriptor(level, region, tornadoPos)) {
- source.sendSuccess(
- () -> Component.literal("ðŸŒªï¸ Tornado engaged once a SimpleClouds cumulonimbus entered range."), true);
- return;
- }
- if (remainingPolls - 1 > 0) {
- projectatmosphere$awaitCloud(source, level, tornadoPos, remainingPolls - 1);
- } else {
- source.sendFailure(Component.literal("âš ï¸ Timed out waiting for a suitable SimpleClouds cumulonimbus."));
- }
- });
+
+ Vec3 playerPos = player.position();
+ double maxDistanceSq = maxDistance * maxDistance;
+ TornadoInstance tornado = TornadoManager.getActiveTornadoes().stream()
+ .filter(t -> t.position.distanceToSqr(playerPos) <= maxDistanceSq)
+ .min(Comparator.comparingDouble(t -> t.position.distanceToSqr(playerPos)))
+ .orElse(null);
+ if (tornado == null) {
+ source.sendFailure(Component.literal("No tornado found within " + Mth.floor(maxDistance) + " blocks."));
+ return 0;
+ }
+
+ int distance = Mth.floor(Math.sqrt(tornado.position.distanceToSqr(playerPos)));
+ TornadoManager.removeTornado(tornado);
+ source.sendSuccess(() -> Component.literal("Removed tornado " + distance + " blocks away."), true);
+ return 1;
+ }
+
+ private static int projectatmosphere$clearAllTornadoes(CommandSourceStack source) {
+ TornadoManager.clearTornadoes();
+ source.sendSuccess(() -> Component.literal("All tornadoes cleared."), true);
+ return 1;
}
private static CloudRegion projectatmosphere$findCumulonimbus(ServerLevel level, Vec3 pos) {
- SpawnRegion region = new SpawnRegion((int) Math.floor(pos.x), (int) Math.floor(pos.z),
- PROJECTATMOSPHERE$SEARCH_RADIUS);
+ SpawnRegion region = new SpawnRegion(
+ (int) Math.floor(pos.x),
+ (int) Math.floor(pos.z),
+ PROJECTATMOSPHERE$SEARCH_RADIUS
+ );
for (CloudRegion cloud : CloudManager.get(level).getClouds()) {
if (!PROJECTATMOSPHERE$CUMULONIMBUS_ID.equals(cloud.getCloudTypeId().toString())) {
continue;
@@ -161,30 +212,4 @@ public static void appendTo(LiteralArgumentBuilder root) {
}
return null;
}
-
- private static boolean projectatmosphere$attachDescriptor(ServerLevel level, CloudRegion region, Vec3 tornadoPos) {
- if (!(region instanceof ITornadoRegion tornadoRegion)) {
- return false;
- }
- float offsetX = (float) (tornadoPos.x - region.getWorldX());
- float offsetZ = (float) (tornadoPos.z - region.getWorldZ());
- float cappedRadius = (float) Math.max(2.0F,
- Math.min(PROJECTATMOSPHERE$DEFAULT_RADIUS, region.getWorldRadius()));
- float bottom = (float) Math.min(tornadoPos.y, level.getSeaLevel());
- float height = Math.max(80.0F, cappedRadius * 12.0F);
- tornadoRegion.getTornadoes().removeIf(descriptor ->
- PROJECTATMOSPHERE$TORNADO_CONTROLLER.equals(descriptor.getControllerId()));
- TornadoDescriptor descriptor = new TornadoDescriptor(
- PROJECTATMOSPHERE$TORNADO_CONTROLLER,
- offsetX,
- offsetZ,
- 0.0F,
- 0.0F,
- cappedRadius,
- bottom,
- height
- );
- tornadoRegion.addTornado(descriptor);
- return true;
- }
}
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoDebug.java b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoDebug.java
index 565340e4..4a5935dc 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoDebug.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoDebug.java
@@ -1,19 +1,23 @@
package net.Gabou.projectatmosphere.modules.tornado;
+import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.FloatArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.Gabou.projectatmosphere.api.WindVectorApi;
import net.Gabou.projectatmosphere.compat.SimpleCloudsCompat;
+import net.Gabou.projectatmosphere.config.AtmoCommonConfig;
import net.Gabou.projectatmosphere.modules.temperature.command.TemperatureCommandHelper;
import net.Gabou.projectatmosphere.util.AtmosphereUtils;
import net.Gabou.projectatmosphere.util.RegionInstanceKey;
import net.Gabou.projectatmosphere.data.TornadoStorageManager;
+import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.Mth;
public final class TornadoDebug {
private TornadoDebug() {}
@@ -68,6 +72,51 @@ public static void appendTo(LiteralArgumentBuilder root) {
() -> Component.literal("Risk: " + risk), false);
return 1;
}))
+ .then(Commands.literal("runtime")
+ .executes(ctx -> {
+ ServerPlayer player = ctx.getSource().getPlayerOrException();
+ ServerLevel level = player.serverLevel();
+ TornadoInstance tornado = TornadoManager.getActiveTornadoes().stream()
+ .min((left, right) -> Double.compare(
+ left.position.distanceToSqr(player.position()),
+ right.position.distanceToSqr(player.position())
+ ))
+ .orElse(null);
+ if (tornado == null) {
+ ctx.getSource().sendFailure(Component.literal("No active tornado found."));
+ return 0;
+ }
+
+ TornadoInstance.RuntimeDebugSnapshot debug = tornado.getRuntimeDebugSnapshot();
+ ctx.getSource().sendSuccess(() -> Component.literal(
+ "Tornado Runtime: " + debug.id() +
+ "\n Phase: " + debug.phase() +
+ "\n Intensity: " + String.format(java.util.Locale.ROOT, "%.3f", debug.normalizedIntensity()) +
+ "\n Eligible entities: " + debug.eligibleEntityCount() +
+ "\n Captured entities: " + debug.capturedEntityCount() +
+ "\n Pull force avg/max: " + String.format(java.util.Locale.ROOT, "%.3f / %.3f", debug.averagePullForce(), debug.maxPullForce()) +
+ "\n Upward force avg/max: " + String.format(java.util.Locale.ROOT, "%.3f / %.3f", debug.averageUpwardForce(), debug.maxUpwardForce()) +
+ "\n Sweep radius: " + String.format(java.util.Locale.ROOT, "%.3f", debug.destructionSweepRadius()) +
+ "\n Candidate blocks: " + debug.destructionCandidateBlockCount() +
+ "\n Destroyed blocks: " + debug.destroyedBlockCount() +
+ "\n Destroyed detail: leaves/logs=" + debug.destroyedLeafLogCount()
+ + " weak=" + debug.destroyedWeakCount()
+ + " grass=" + debug.destroyedGrassCount()
+ + " glass=" + debug.destroyedGlassCount()
+ ).withStyle(ChatFormatting.YELLOW), false);
+ return 1;
+ }))
+ .then(Commands.literal("logging")
+ .then(Commands.argument("value", BoolArgumentType.bool())
+ .executes(ctx -> {
+ boolean value = BoolArgumentType.getBool(ctx, "value");
+ AtmoCommonConfig.TORNADO_DEBUG_LOGGING.set(value);
+ ctx.getSource().sendSuccess(
+ () -> Component.literal("Tornado runtime logging set to: " + value),
+ true
+ );
+ return 1;
+ })))
.then(Commands.literal("force")
.then(Commands.argument("intensity", FloatArgumentType.floatArg(0f, 1f))
.executes(ctx -> {
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoInstance.java b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoInstance.java
index 75bae94c..4e7a1d5f 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoInstance.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoInstance.java
@@ -1,285 +1,1578 @@
package net.Gabou.projectatmosphere.modules.tornado;
-
import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudRegion;
-import net.Gabou.projectatmosphere.util.AsyncAtmosphereService;
+import net.Gabou.projectatmosphere.api.common.cloud.region.ITornadoRegion;
+import net.Gabou.projectatmosphere.api.common.cloud.region.TornadoDescriptor;
+import net.Gabou.projectatmosphere.config.AtmoCommonConfig;
+import net.Gabou.projectatmosphere.manager.ForecastOrchestrator;
+import net.Gabou.projectatmosphere.modules.core.WindVector;
+import net.Gabou.projectatmosphere.modules.weather.StormLifecyclePhase;
+import net.Gabou.projectatmosphere.modules.weather.StormMotionModel;
+import net.Gabou.projectatmosphere.modules.weather.StormSeverityScale;
+import net.Gabou.projectatmosphere.modules.weather.StormShieldManager;
import net.Gabou.projectatmosphere.util.AtmosphereUtils;
import net.minecraft.core.BlockPos;
+import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.tags.BlockTags;
+import net.minecraft.tags.FluidTags;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.item.FallingBlockEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
-import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
+import org.jetbrains.annotations.Nullable;
-import net.Gabou.projectatmosphere.modules.core.WindVector;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
public class TornadoInstance {
+ public static final double AMBIENT_WIND_INFLUENCE_EXTENSION = 15.0D;
+ public static final double WIND_SPEED_SCALING_FACTOR = 0.05D;
+ public static final double WIND_EFFECT_VERTICAL_MAX_OFFSET = 50.0D;
+ public static final double WIND_EFFECT_VERTICAL_MIN_OFFSET = -5.0D;
+ private static final int MINIMUM_PERSISTENCE_TICKS = 20 * 120;
+ private static final int MINIMUM_ACTIVE_TICKS = 20 * 120;
+ private static final int MAXIMUM_ACTIVE_TICKS = 20 * 600;
+ private static final int MINIMUM_FORMATION_TICKS = 20 * 6;
+ private static final int MAXIMUM_FORMATION_TICKS = 20 * 20;
+ private static final int MINIMUM_DISSIPATION_TICKS = 20 * 8;
+ private static final int MAXIMUM_DISSIPATION_TICKS = 20 * 40;
+ private static final int FLOW_FIELD_INTERVAL_TICKS = 1;
+ private static final int DEMOLITION_INTERVAL_TICKS = 4;
+ private static final float MIN_EFFECTIVE_WIND = 73.0F;
+ private static final float MAX_EFFECTIVE_WIND = 260.0F;
+ private static final double OUTER_ENTITY_INFLUENCE_PADDING = 16.0D;
+ private static final double ENTITY_MIN_VERTICAL_RANGE = -8.0D;
+ private static final double ENTITY_MAX_VERTICAL_PADDING = 20.0D;
+ private static final double ENTITY_RELEASE_HEIGHT_PADDING = 10.0D;
+ private static final float CAPTURE_RADIUS_FACTOR = 0.84F;
+ private static final float CORE_RADIUS_FACTOR = 0.34F;
+ private static final float OUTER_ORBIT_RADIUS_FACTOR = 0.60F;
+ private static final float INNER_ORBIT_RADIUS_FACTOR = 0.30F;
+ private static final float CAPTURE_HYSTERESIS_FACTOR = 1.18F;
+ private static final int CAPTURE_FULL_TICKS = 24;
+ private static final int CAPTURE_ASCENT_TICKS = 90;
+ private static final int CAPTURE_RELEASE_TICKS = 220;
+ private static final float BASE_SUCTION_FORCE = 0.13F;
+ private static final float BASE_TANGENTIAL_FORCE = 0.11F;
+ private static final float BASE_LIFT_FORCE = 0.12F;
+ private static final float CAPTURED_SUCTION_FORCE = 0.25F;
+ private static final float CAPTURED_TANGENTIAL_FORCE = 0.23F;
+ private static final float CAPTURED_LIFT_FORCE = 0.34F;
+ private static final float WATER_PENALTY_THRESHOLD = 0.20F;
+ private static final int CLOUD_DETACH_GRACE_TICKS = 20 * 30;
+ private static final float CLIENT_POSITION_INTERPOLATION = 0.18F;
+ private static final float CLIENT_SHAPE_INTERPOLATION = 0.22F;
+ private static final double CLIENT_SNAPSHOT_INTERVAL_TICKS = 5.0D;
+ private static final double CLIENT_VELOCITY_TRACKING = 0.45D;
+ private static final double CLIENT_VELOCITY_DAMPING = 0.84D;
+ private static final double CLIENT_EXTRAPOLATION_TICKS = 1.35D;
+ private static final int TREE_CLUSTER_HORIZONTAL_RADIUS = 4;
+ private static final int TREE_CLUSTER_BELOW = 4;
+ private static final int TREE_CLUSTER_ABOVE = 20;
+ private static final int TREE_CLUSTER_VISIT_LIMIT = 512;
+ private static final float MAX_DEBRIS_ENTITY_SPAWN_CHANCE = 0.46F;
+ private static final int BASE_MAX_DEBRIS_ENTITY_SPAWNS = 8;
+ private static final int ENTITY_DAMAGE_INTERVAL_TICKS = 8;
+ private static final float MOVEMENT_ROUTE_LEASH_RADIUS = 150.0F;
+ private static final double MOVEMENT_ROUTE_REACHED_DISTANCE_SQR = 36.0D;
+ private static final float MOVEMENT_HEADING_BLEND = 0.010F;
+ private static final float MOVEMENT_SPEED_BLEND = 0.10F;
+ private static final double MOVEMENT_VECTOR_BLEND = 0.16D;
+ private static final double MOVEMENT_AMBIENT_SCALE = 0.0035D;
+ private static final int MOVEMENT_REPLAN_ON_AVOIDANCE_TICKS = 24;
- private static final int DEBRIS_RANGE_EXTENSION = 5;
- public static final double AMBIENT_WIND_INFLUENCE_EXTENSION= 15;
- public static final double WIND_SPEED_SCALING_FACTOR= 0.05;
- public static final double WIND_EFFECT_VERTICAL_MAX_OFFSET = 50;
- public static final double WIND_EFFECT_VERTICAL_MIN_OFFSET= -5;
+ private final UUID id;
public Vec3 position;
- public final long spawnTime;
- public final float radius;
- public final WindVector wind;
+ public float radius;
+ public WindVector wind;
+ private final float maxRadius;
+ private final float targetVisualHeight;
+ private float visualBottomY;
+ private float visualHeight;
+ private float angularSpeed;
+ private float normalizedIntensity;
+ private float targetIntensity;
+ private int stormLevel;
+ private float headingRadians;
+ private float targetHeadingRadians;
+ private Vec3 motion = Vec3.ZERO;
+ private float plannedMoveSpeed;
+ private float targetMoveSpeed;
+ private int routeTicksRemaining;
+ @Nullable
+ private Vec3 routeWaypoint;
+ private boolean descriptorMissing;
+ private int ageTicks;
+ private int phaseTicks;
+ private int formationTicks;
+ private int activeTicks;
+ private int dissipationTicks;
+ private int detachedTicks;
+ private long spawnGameTime;
+ private long lastAmbientWindTick = 0;
+ private long lastDemolitionTick = 0;
+ private float anchorX;
+ private float anchorZ;
+ private final boolean requiresCloudAttachment;
+ private StormLifecyclePhase phase;
+ private float recentDebrisScore;
+ private Vec3 clientPreviousRenderPosition;
+ private Vec3 clientRenderPosition;
+ private Vec3 clientTargetPosition;
+ private Vec3 clientTargetVelocity;
+ private float clientPreviousRenderBottomY;
+ private float clientRenderBottomY;
+ private float clientTargetBottomY;
+ private float clientPreviousRenderHeight;
+ private float clientRenderHeight;
+ private float clientTargetHeight;
+ private float clientPreviousRenderRadius;
+ private float clientRenderRadius;
+ private float clientTargetRadius;
+ private final Map capturedEntities = new HashMap<>();
+ private int debugEligibleEntityCount;
+ private int debugCapturedEntityCount;
+ private int debugForceSampleCount;
+ private double debugPullForceSum;
+ private double debugUpwardForceSum;
+ private double debugPullForceMax;
+ private double debugUpwardForceMax;
+ private float debugDestructionSweepRadius;
+ private int debugDestructionCandidateBlockCount;
+ private int debugDestroyedBlockCount;
+ private int debugDestroyedLeafLogCount;
+ private int debugDestroyedWeakCount;
+ private int debugDestroyedGrassCount;
+ private int debugDestroyedGlassCount;
- private final CloudRegion cloudRegion;
+ @Nullable
+ private CloudRegion cloudRegion;
- private float angularSpeed = 0.15f;
- private long lastDemolitionCheck = 0L;
- private final long demolitionIntervalMs = 1000L;
- private final long ambientWindIntervalMs = 2000L;
- private long lastAmbientWindCheck = 0L;
+ public TornadoInstance(Vec3 position, float radius, WindVector wind, @Nullable CloudRegion cloudRegion) {
+ this(UUID.randomUUID(), position, radius, wind, 0.05F, (float) position.y, Math.max(96.0F, radius * 12.0F), cloudRegion, StormSeverityScale.fromNormalized(Mth.clamp((radius - 5.0F) / 20.0F, 0.25F, 1.0F)));
+ }
+ public TornadoInstance(UUID id, Vec3 position, float radius, WindVector wind,
+ float visualBottomY, float visualHeight, @Nullable CloudRegion cloudRegion) {
+ this(id, position, radius, wind, 0.05F, visualBottomY, visualHeight, cloudRegion, StormSeverityScale.fromNormalized(Mth.clamp((radius - 5.0F) / 20.0F, 0.25F, 1.0F)));
+ }
- private final TornadoLevel level;
+ public TornadoInstance(UUID id, Vec3 position, float radius, WindVector wind, float angularSpeed,
+ float visualBottomY, float visualHeight, @Nullable CloudRegion cloudRegion) {
+ this(id, position, radius, wind, angularSpeed, visualBottomY, visualHeight, cloudRegion, StormSeverityScale.fromNormalized(Mth.clamp((radius - 5.0F) / 20.0F, 0.25F, 1.0F)));
+ }
+ public TornadoInstance(UUID id, Vec3 position, float radius, WindVector wind, float angularSpeed,
+ float visualBottomY, float visualHeight, @Nullable CloudRegion cloudRegion, int stormLevel) {
+ this(id, position, radius, wind, angularSpeed, visualBottomY, visualHeight, cloudRegion, stormLevel, true);
+ }
- public TornadoLevel getLevel() {
- return level;
+ public TornadoInstance(UUID id, Vec3 position, float radius, WindVector wind, float angularSpeed,
+ float visualBottomY, float visualHeight, @Nullable CloudRegion cloudRegion, int stormLevel,
+ boolean requiresCloudAttachment) {
+ this.id = id;
+ this.position = position;
+ this.radius = radius;
+ this.maxRadius = radius;
+ this.wind = wind;
+ this.angularSpeed = angularSpeed;
+ this.visualBottomY = visualBottomY;
+ this.visualHeight = visualHeight;
+ this.targetVisualHeight = Math.max(visualHeight, 32.0F);
+ this.cloudRegion = cloudRegion;
+ this.stormLevel = StormSeverityScale.clamp(stormLevel);
+ this.requiresCloudAttachment = requiresCloudAttachment;
+ this.anchorX = (float) position.x;
+ this.anchorZ = (float) position.z;
+ this.phase = StormLifecyclePhase.FORMING;
+ this.targetIntensity = defaultTargetIntensity(radius, wind, this.stormLevel);
+ this.normalizedIntensity = Math.max(0.18F, this.targetIntensity * 0.35F);
+ this.headingRadians = wind.angleRadians();
+ this.targetHeadingRadians = this.headingRadians;
+ this.plannedMoveSpeed = 0.04F + this.targetIntensity * 0.05F;
+ this.targetMoveSpeed = this.plannedMoveSpeed;
+ this.routeTicksRemaining = 0;
+ this.routeWaypoint = null;
+ float persistenceFactor = Mth.clamp(
+ this.targetIntensity * 0.65F + StormSeverityScale.toNormalized(this.stormLevel) * 0.35F,
+ 0.0F,
+ 1.0F
+ );
+ this.formationTicks = Mth.floor(Mth.lerp(persistenceFactor, MINIMUM_FORMATION_TICKS, MAXIMUM_FORMATION_TICKS));
+ this.activeTicks = Mth.floor(Mth.lerp(persistenceFactor, MINIMUM_ACTIVE_TICKS, MAXIMUM_ACTIVE_TICKS));
+ this.dissipationTicks = Mth.floor(Mth.lerp(persistenceFactor, MINIMUM_DISSIPATION_TICKS, MAXIMUM_DISSIPATION_TICKS));
+ this.applyIntensityToVisuals();
+ this.clientPreviousRenderPosition = position;
+ this.clientRenderPosition = position;
+ this.clientTargetPosition = position;
+ this.clientTargetVelocity = Vec3.ZERO;
+ this.clientPreviousRenderBottomY = this.visualBottomY;
+ this.clientRenderBottomY = this.visualBottomY;
+ this.clientTargetBottomY = this.visualBottomY;
+ this.clientPreviousRenderHeight = this.visualHeight;
+ this.clientRenderHeight = this.visualHeight;
+ this.clientTargetHeight = this.visualHeight;
+ this.clientPreviousRenderRadius = this.radius;
+ this.clientRenderRadius = this.radius;
+ this.clientTargetRadius = this.radius;
+ }
+
+ public UUID getId() {
+ return this.id;
}
+ @Nullable
public CloudRegion getCloudRegion() {
- return cloudRegion;
+ return this.cloudRegion;
}
- public double getSuctionRadius() {
- return level.getBaseDamage() * 2;
+ public void setCloudRegion(@Nullable CloudRegion cloudRegion) {
+ this.cloudRegion = cloudRegion;
+ this.detachedTicks = cloudRegion == null ? this.detachedTicks : 0;
}
- public double getDamageMultiplier() {
- return level.getBaseDamage();
+ public float getVisualBottomY() {
+ return this.visualBottomY;
}
- public TornadoInstance(Vec3 position, float radius, WindVector wind,CloudRegion cloudRegion) {
- this(position, radius, wind, 0.05f,cloudRegion);
+ public float getVisualHeight() {
+ return this.visualHeight;
}
- public TornadoInstance(Vec3 position, float radius, WindVector wind, float angularSpeed, CloudRegion cloudRegion) {
- this.position = position;
- this.radius = radius;
- this.wind = wind;
- this.angularSpeed = angularSpeed;
- this.spawnTime = System.currentTimeMillis();
- this.level = TornadoLevel.fromWindSpeed(wind.baseSpeed());
- this.cloudRegion = cloudRegion;
+ public float getNormalizedIntensity() {
+ return this.normalizedIntensity;
+ }
+ public StormLifecyclePhase getPhase() {
+ return this.phase;
+ }
+
+ public int getStormLevel() {
+ return this.stormLevel;
+ }
+
+ public float getRecentDebrisScore() {
+ return this.recentDebrisScore;
+ }
+
+ public Vec3 getRenderPosition(float partialTick) {
+ return this.clientPreviousRenderPosition.lerp(this.clientRenderPosition, Mth.clamp(partialTick, 0.0F, 1.0F));
+ }
+
+ public float getRenderBottomY(float partialTick) {
+ return Mth.lerp(Mth.clamp(partialTick, 0.0F, 1.0F), this.clientPreviousRenderBottomY, this.clientRenderBottomY);
+ }
+
+ public float getRenderHeight(float partialTick) {
+ return Mth.lerp(Mth.clamp(partialTick, 0.0F, 1.0F), this.clientPreviousRenderHeight, this.clientRenderHeight);
+ }
+
+ public float getRenderRadius(float partialTick) {
+ return Mth.lerp(Mth.clamp(partialTick, 0.0F, 1.0F), this.clientPreviousRenderRadius, this.clientRenderRadius);
+ }
+
+ public TornadoLevel getLevel() {
+ return TornadoLevel.fromWindSpeed(this.getEffectiveWindSpeed());
+ }
+
+ public double getSuctionRadius() {
+ return this.radius + this.getLevel().getBaseDamage() * 1.2D;
+ }
+
+ public double getDamageMultiplier() {
+ return this.getLevel().getBaseDamage()
+ * Math.max(0.3D, this.normalizedIntensity)
+ * (0.75D + StormSeverityScale.toNormalized(this.stormLevel) * 0.65D);
}
public float getLifetimeSeconds() {
- return (System.currentTimeMillis() - spawnTime) / 1000f;
+ return this.ageTicks / 20.0F;
}
public float getTwist() {
- long elapsedMs = System.currentTimeMillis() - spawnTime;
- float elapsedTicks = elapsedMs / 100.0f;
- return Mth.clamp(elapsedTicks * angularSpeed,0.5f,5.0f);
+ return this.getVisualSpin(0.0F);
}
- /**
- * Called each tick from tornado manager. Server handles demolition,
- * client relies on separate rendering logic.
- */
- public void tick(Level level) {
- if (level.isClientSide) {
+ public float getVisualSpin(float partialTick) {
+ float elapsedTicks = this.ageTicks + Mth.clamp(partialTick, 0.0F, 1.0F);
+ return elapsedTicks * (0.004F + this.angularSpeed * 0.16F);
+ }
+
+ public boolean isDescriptorMissing() {
+ return this.descriptorMissing;
+ }
+
+ public void markDissipating() {
+ if (this.phase == StormLifecyclePhase.DISSIPATED || this.phase == StormLifecyclePhase.DISSIPATING) {
return;
}
+ this.phase = StormLifecyclePhase.DISSIPATING;
+ this.phaseTicks = 0;
+ }
- long now = System.currentTimeMillis();
+ public void activateImmediately() {
+ this.phase = StormLifecyclePhase.ACTIVE;
+ this.phaseTicks = 0;
+ this.normalizedIntensity = this.targetIntensity;
+ this.applyIntensityToVisuals();
+ }
- boolean ambientDue = now - lastAmbientWindCheck >= ambientWindIntervalMs;
- boolean demolitionDue = now - lastDemolitionCheck >= demolitionIntervalMs;
+ public boolean isDead() {
+ return this.phase.isTerminal();
+ }
- if (ambientDue || demolitionDue) {
- AsyncAtmosphereService.runStorm(() -> {
- try {
- if (ambientDue) {
- lastAmbientWindCheck = now;
- applyAmbientWind(level);
- }
- if (demolitionDue) {
- lastDemolitionCheck = now;
- demolishBlocks((ServerLevel) level);
- level.getServer().execute(()->playDemolitionSound(level));
- }
- } catch (Exception e) {
- e.printStackTrace();
+ public void updateCloudAttachment(boolean attached) {
+ if (!this.requiresCloudAttachment) {
+ this.detachedTicks = 0;
+ return;
+ }
+ if (attached) {
+ this.detachedTicks = 0;
+ return;
+ }
+
+ this.detachedTicks++;
+ if (this.detachedTicks >= CLOUD_DETACH_GRACE_TICKS
+ && this.ageTicks >= MINIMUM_PERSISTENCE_TICKS
+ && this.phase != StormLifecyclePhase.DISSIPATING) {
+ this.markDissipating();
+ }
+ }
+
+ public int getDetachedTicks() {
+ return this.detachedTicks;
+ }
+
+ public RuntimeDebugSnapshot getRuntimeDebugSnapshot() {
+ double averagePullForce = this.debugForceSampleCount <= 0 ? 0.0D : this.debugPullForceSum / this.debugForceSampleCount;
+ double averageUpwardForce = this.debugForceSampleCount <= 0 ? 0.0D : this.debugUpwardForceSum / this.debugForceSampleCount;
+ return new RuntimeDebugSnapshot(
+ this.id,
+ this.phase,
+ this.normalizedIntensity,
+ this.debugEligibleEntityCount,
+ this.debugCapturedEntityCount,
+ averagePullForce,
+ this.debugPullForceMax,
+ averageUpwardForce,
+ this.debugUpwardForceMax,
+ this.debugDestructionSweepRadius,
+ this.debugDestructionCandidateBlockCount,
+ this.debugDestroyedBlockCount,
+ this.debugDestroyedLeafLogCount,
+ this.debugDestroyedWeakCount,
+ this.debugDestroyedGrassCount,
+ this.debugDestroyedGlassCount
+ );
+ }
+
+ public void tickServer(ServerLevel level, long gameTime) {
+ this.ageTicks++;
+ this.phaseTicks++;
+ if (this.spawnGameTime == 0L) {
+ this.spawnGameTime = gameTime;
+ }
+
+ this.resetRuntimeDebugStats();
+
+ WindVector sampledWind = ForecastOrchestrator.getWind(level, BlockPos.containing(this.position), gameTime);
+ this.wind = sampledWind;
+ this.refreshStormLevel(level, gameTime);
+ float waterExposure = this.sampleWaterExposure(level);
+ this.applyWaterPenalty(waterExposure);
+ this.recentDebrisScore = Math.max(0.0F, this.recentDebrisScore - 0.015F);
+ this.pruneCapturedEntities(level);
+ this.tickLifecycle();
+ this.updateMovement(level, gameTime);
+ this.pushStateToDescriptor();
+
+ if (!this.phase.isTerminal() && this.normalizedIntensity >= 0.08F) {
+ if (gameTime - this.lastAmbientWindTick >= FLOW_FIELD_INTERVAL_TICKS) {
+ this.lastAmbientWindTick = gameTime;
+ this.applyAmbientWind(level);
+ }
+ if (gameTime - this.lastDemolitionTick >= DEMOLITION_INTERVAL_TICKS && this.normalizedIntensity >= 0.18F) {
+ this.lastDemolitionTick = gameTime;
+ if (this.demolishBlocks(level)) {
+ this.playDemolitionSound(level);
}
- });
+ }
}
+ }
+ public void tickClient() {
+ this.ageTicks++;
+ this.clientPreviousRenderPosition = this.clientRenderPosition;
+ this.clientPreviousRenderBottomY = this.clientRenderBottomY;
+ this.clientPreviousRenderHeight = this.clientRenderHeight;
+ this.clientPreviousRenderRadius = this.clientRenderRadius;
+
+ Vec3 predictedTarget = this.clientTargetPosition.add(this.clientTargetVelocity.scale(CLIENT_EXTRAPOLATION_TICKS));
+ this.clientRenderPosition = this.clientRenderPosition.lerp(predictedTarget, CLIENT_POSITION_INTERPOLATION);
+ this.clientRenderBottomY = Mth.lerp(CLIENT_SHAPE_INTERPOLATION, this.clientRenderBottomY, this.clientTargetBottomY);
+ this.clientRenderHeight = Mth.lerp(CLIENT_SHAPE_INTERPOLATION, this.clientRenderHeight, this.clientTargetHeight);
+ this.clientRenderRadius = Mth.lerp(CLIENT_SHAPE_INTERPOLATION, this.clientRenderRadius, this.clientTargetRadius);
+ this.clientTargetVelocity = this.clientTargetVelocity.scale(CLIENT_VELOCITY_DAMPING);
}
+ public TornadoSnapshot snapshot() {
+ return new TornadoSnapshot(
+ this.id,
+ this.position,
+ this.radius,
+ this.visualBottomY,
+ this.visualHeight,
+ this.wind.baseSpeed(),
+ this.wind.angleRadians(),
+ this.wind.gustSpeed(),
+ this.normalizedIntensity,
+ this.stormLevel,
+ this.recentDebrisScore,
+ this.phase
+ );
+ }
+
+ public void applySnapshot(TornadoSnapshot snapshot, @Nullable CloudRegion region) {
+ boolean snapToTarget = this.ageTicks <= 1 || this.clientRenderPosition.distanceToSqr(snapshot.position()) > 1024.0D;
+ Vec3 previousTargetPosition = this.clientTargetPosition;
+ this.position = snapshot.position();
+ this.radius = snapshot.radius();
+ this.visualBottomY = snapshot.visualBottomY();
+ this.visualHeight = snapshot.visualHeight();
+ this.wind = new WindVector(snapshot.windSpeed(), snapshot.windAngle(), snapshot.windGust());
+ this.normalizedIntensity = snapshot.normalizedIntensity();
+ this.stormLevel = StormSeverityScale.clamp(snapshot.stormLevel());
+ this.recentDebrisScore = snapshot.recentDebrisScore();
+ this.phase = snapshot.phase();
+ this.cloudRegion = region;
+ this.anchorX = (float) this.position.x;
+ this.anchorZ = (float) this.position.z;
+ if (snapToTarget) {
+ this.clientPreviousRenderPosition = this.position;
+ this.clientRenderPosition = this.position;
+ this.clientTargetPosition = this.position;
+ this.clientTargetVelocity = Vec3.ZERO;
+ this.clientPreviousRenderBottomY = this.visualBottomY;
+ this.clientRenderBottomY = this.visualBottomY;
+ this.clientTargetBottomY = this.visualBottomY;
+ this.clientPreviousRenderHeight = this.visualHeight;
+ this.clientRenderHeight = this.visualHeight;
+ this.clientTargetHeight = this.visualHeight;
+ this.clientPreviousRenderRadius = this.radius;
+ this.clientRenderRadius = this.radius;
+ this.clientTargetRadius = this.radius;
+ return;
+ }
+
+ Vec3 snapshotVelocity = this.position.subtract(previousTargetPosition).scale(1.0D / CLIENT_SNAPSHOT_INTERVAL_TICKS);
+ this.clientTargetPosition = this.position;
+ this.clientTargetVelocity = this.clientTargetVelocity.lerp(snapshotVelocity, CLIENT_VELOCITY_TRACKING);
+ this.clientTargetBottomY = this.visualBottomY;
+ this.clientTargetHeight = this.visualHeight;
+ this.clientTargetRadius = this.radius;
+ }
+
+ public boolean synchronizeWithDescriptor() {
+ TornadoDescriptor descriptor = this.findDescriptor();
+ if (descriptor == null) {
+ this.descriptorMissing = this.cloudRegion instanceof ITornadoRegion;
+ return false;
+ }
+
+ this.descriptorMissing = false;
+ this.visualBottomY = descriptor.getBottomY();
+ this.visualHeight = descriptor.getHeight();
+ this.radius = descriptor.getRadius();
+ this.position = new Vec3(
+ this.cloudRegion.getWorldX() + descriptor.getOffsetX(),
+ descriptor.getBottomY(),
+ this.cloudRegion.getWorldZ() + descriptor.getOffsetZ()
+ );
+ return true;
+ }
+
+ public void advanceByWind() {
+ this.ensureFallbackMovementPlan();
+ this.advanceAlongMovementPlan(null);
+ }
+
+ private void tickLifecycle() {
+ switch (this.phase) {
+ case FORMING -> {
+ float rate = 1.0F / Math.max(1, this.formationTicks);
+ this.normalizedIntensity = Math.min(this.targetIntensity, this.normalizedIntensity + rate);
+ if (this.phaseTicks >= this.formationTicks || this.normalizedIntensity >= this.targetIntensity - 0.02F) {
+ this.phase = StormLifecyclePhase.ACTIVE;
+ this.phaseTicks = 0;
+ }
+ }
+ case ACTIVE -> {
+ this.normalizedIntensity = Mth.lerp(0.06F, this.normalizedIntensity, this.targetIntensity);
+ if (this.phaseTicks >= this.activeTicks) {
+ this.phase = StormLifecyclePhase.DISSIPATING;
+ this.phaseTicks = 0;
+ }
+ }
+ case DISSIPATING -> {
+ float rate = 1.0F / Math.max(1, this.dissipationTicks);
+ this.normalizedIntensity = Math.max(0.0F, this.normalizedIntensity - rate);
+ if (this.phaseTicks >= this.dissipationTicks || this.normalizedIntensity <= 0.02F) {
+ this.phase = StormLifecyclePhase.DISSIPATED;
+ this.normalizedIntensity = 0.0F;
+ }
+ }
+ case DISSIPATED -> this.normalizedIntensity = 0.0F;
+ }
+ this.applyIntensityToVisuals();
+ }
+
+ private void applyIntensityToVisuals() {
+ float growth = Mth.clamp(this.normalizedIntensity, 0.0F, 1.0F);
+ float stormBias = 0.18F + StormSeverityScale.toNormalized(this.stormLevel) * 0.82F;
+ this.radius = Mth.lerp(growth, this.maxRadius * (0.26F + stormBias * 0.12F), this.maxRadius * (0.90F + stormBias * 0.22F));
+ this.visualHeight = Mth.lerp(growth, this.targetVisualHeight * (0.30F + stormBias * 0.08F), this.targetVisualHeight * (0.92F + stormBias * 0.15F));
+ this.angularSpeed = 0.08F + growth * 0.16F + StormSeverityScale.toNormalized(this.stormLevel) * 0.05F;
+ }
+
+ private void updateMovement(ServerLevel level, long gameTime) {
+ // Route selection runs on a slower cadence. Per-tick movement only blends toward the
+ // current plan so the tornado keeps committing to a path instead of re-steering every tick.
+ this.ensureMovementPlan(level, gameTime);
+ this.advanceAlongMovementPlan(level);
+ }
+
+ private void ensureMovementPlan(ServerLevel level, long gameTime) {
+ if (!this.shouldReplanMovement(level)) {
+ return;
+ }
+ this.applyMovementPlan(StormMotionModel.planTornadoRoute(
+ level,
+ this.id,
+ this.position,
+ this.wind,
+ Math.max(this.normalizedIntensity, 0.08F),
+ this.stormLevel,
+ this.headingRadians,
+ gameTime,
+ this.anchorX,
+ this.anchorZ
+ ));
+ }
+
+ private void ensureFallbackMovementPlan() {
+ if (!this.shouldReplanMovement(null)) {
+ return;
+ }
+ this.applyMovementPlan(StormMotionModel.planFallbackTornadoRoute(
+ this.id,
+ this.position,
+ this.wind,
+ Math.max(this.normalizedIntensity, 0.08F),
+ this.stormLevel,
+ this.headingRadians,
+ this.ageTicks,
+ this.anchorX,
+ this.anchorZ
+ ));
+ }
+
+ private boolean shouldReplanMovement(@Nullable ServerLevel level) {
+ if (this.routeWaypoint == null || this.routeTicksRemaining <= 0) {
+ return true;
+ }
+ if (this.hasReachedRouteWaypoint()) {
+ return true;
+ }
+ if (level != null && StormShieldManager.isProtected(level, this.routeWaypoint)) {
+ return true;
+ }
+ double dx = this.routeWaypoint.x - this.anchorX;
+ double dz = this.routeWaypoint.z - this.anchorZ;
+ return dx * dx + dz * dz > MOVEMENT_ROUTE_LEASH_RADIUS * MOVEMENT_ROUTE_LEASH_RADIUS;
+ }
+
+ private boolean hasReachedRouteWaypoint() {
+ if (this.routeWaypoint == null) {
+ return true;
+ }
+ double dx = this.routeWaypoint.x - this.position.x;
+ double dz = this.routeWaypoint.z - this.position.z;
+ double dynamicThreshold = Math.max(MOVEMENT_ROUTE_REACHED_DISTANCE_SQR, this.motion.lengthSqr() * 48.0D);
+ return dx * dx + dz * dz <= dynamicThreshold;
+ }
+
+ private void applyMovementPlan(StormMotionModel.TornadoRoutePlan plan) {
+ this.routeWaypoint = new Vec3(plan.waypoint().x, this.visualBottomY, plan.waypoint().z);
+ this.targetHeadingRadians = plan.headingRadians();
+ this.targetMoveSpeed = plan.speed();
+ this.routeTicksRemaining = Math.max(1, plan.durationTicks());
+ if (this.motion.lengthSqr() <= 1.0E-5D) {
+ this.headingRadians = this.targetHeadingRadians;
+ this.plannedMoveSpeed = this.targetMoveSpeed;
+ }
+ }
+
+ private void advanceAlongMovementPlan(@Nullable ServerLevel level) {
+ Vec3 waypoint = this.routeWaypoint != null
+ ? this.routeWaypoint
+ : this.position.add(horizontalVector(this.targetHeadingRadians).scale(12.0D));
+ Vec3 toWaypoint = new Vec3(waypoint.x - this.position.x, 0.0D, waypoint.z - this.position.z);
+ float desiredHeading = toWaypoint.lengthSqr() > 1.0E-4D
+ ? (float) Math.atan2(toWaypoint.z, toWaypoint.x)
+ : this.targetHeadingRadians;
+ float stormFactor = StormSeverityScale.toNormalized(this.stormLevel);
+ float maxTurn = MOVEMENT_HEADING_BLEND
+ + this.normalizedIntensity * 0.012F
+ + stormFactor * 0.008F;
+ this.headingRadians = rotateTowards(this.headingRadians, desiredHeading, maxTurn);
+ this.plannedMoveSpeed = Mth.lerp(MOVEMENT_SPEED_BLEND, this.plannedMoveSpeed, this.targetMoveSpeed);
+
+ Vec3 plannedVector = horizontalVector(this.headingRadians).scale(this.plannedMoveSpeed);
+ Vec3 ambientVector = horizontalVector(this.wind.angleRadians()).scale(
+ Math.max(0.6F, this.wind.baseSpeed()) * (MOVEMENT_AMBIENT_SCALE + this.normalizedIntensity * 0.0012D)
+ );
+ Vec3 leashCorrection = this.sampleMovementLeashCorrection();
+ Vec3 shieldCorrection = Vec3.ZERO;
+ if (level != null) {
+ Vec3 avoidance = StormShieldManager.sampleAvoidance(level, this.position, 24.0D + this.stormLevel * 8.0D);
+ if (avoidance.lengthSqr() > 1.0E-6D) {
+ shieldCorrection = avoidance.scale(0.05D + stormFactor * 0.08D);
+ if (this.routeTicksRemaining > MOVEMENT_REPLAN_ON_AVOIDANCE_TICKS) {
+ this.routeTicksRemaining = MOVEMENT_REPLAN_ON_AVOIDANCE_TICKS;
+ }
+ }
+ }
+
+ Vec3 targetMotion = plannedVector.add(ambientVector).add(leashCorrection).add(shieldCorrection);
+ this.motion = this.motion.lerp(targetMotion, MOVEMENT_VECTOR_BLEND + this.normalizedIntensity * 0.05D);
+ if (this.motion.lengthSqr() <= 1.0E-6D && targetMotion.lengthSqr() > 0.0D) {
+ this.motion = targetMotion;
+ }
+
+ double maxSpeed = Math.max(0.05D, this.targetMoveSpeed * 1.75D + 0.04D);
+ if (this.motion.lengthSqr() > maxSpeed * maxSpeed) {
+ this.motion = this.motion.normalize().scale(maxSpeed);
+ }
+
+ this.position = new Vec3(this.position.x + this.motion.x, this.visualBottomY, this.position.z + this.motion.z);
+ if (this.routeTicksRemaining > 0) {
+ this.routeTicksRemaining--;
+ }
+ }
+
+ private Vec3 sampleMovementLeashCorrection() {
+ double dx = this.anchorX - this.position.x;
+ double dz = this.anchorZ - this.position.z;
+ double distSqr = dx * dx + dz * dz;
+ if (distSqr <= MOVEMENT_ROUTE_LEASH_RADIUS * MOVEMENT_ROUTE_LEASH_RADIUS) {
+ return Vec3.ZERO;
+ }
+ double dist = Math.sqrt(distSqr);
+ double strength = Mth.clamp(
+ (dist - MOVEMENT_ROUTE_LEASH_RADIUS) / (MOVEMENT_ROUTE_LEASH_RADIUS * 0.80D),
+ 0.0D,
+ 1.0D
+ );
+ return new Vec3(dx / Math.max(dist, 0.001D), 0.0D, dz / Math.max(dist, 0.001D))
+ .scale(0.03D + strength * 0.11D);
+ }
+
+ private static Vec3 horizontalVector(float heading) {
+ return new Vec3(Math.cos(heading), 0.0D, Math.sin(heading));
+ }
+
+ private static float rotateTowards(float current, float target, float maxTurn) {
+ float delta = Mth.wrapDegrees((float) Math.toDegrees(target - current));
+ float clamped = Mth.clamp(delta, (float) Math.toDegrees(-maxTurn), (float) Math.toDegrees(maxTurn));
+ return current + (float) Math.toRadians(clamped);
+ }
+
+ private void pushStateToDescriptor() {
+ TornadoDescriptor descriptor = this.findDescriptor();
+ if (descriptor == null) {
+ this.descriptorMissing = this.cloudRegion instanceof ITornadoRegion;
+ return;
+ }
+ this.descriptorMissing = false;
+ descriptor.setBottomY(this.visualBottomY);
+ descriptor.setHeight(this.visualHeight);
+ descriptor.setRadius(this.radius);
+ descriptor.setVelocityX((float) this.motion.x);
+ descriptor.setVelocityZ((float) this.motion.z);
+ if (this.cloudRegion != null) {
+ descriptor.setOffsetX((float) (this.position.x - this.cloudRegion.getWorldX()));
+ descriptor.setOffsetZ((float) (this.position.z - this.cloudRegion.getWorldZ()));
+ }
+ }
private void applyAmbientWind(Level level) {
if (!(level instanceof ServerLevel serverLevel)) {
return;
}
- double influence = radius + AMBIENT_WIND_INFLUENCE_EXTENSION;
- double minY = position.y + WIND_EFFECT_VERTICAL_MIN_OFFSET;
- double maxY = position.y + WIND_EFFECT_VERTICAL_MAX_OFFSET;
+ Vec3 anchor = this.getInteractionAnchor(serverLevel);
+ double influence = this.getOuterInfluenceRadius();
+ double minY = anchor.y + ENTITY_MIN_VERTICAL_RANGE;
+ double maxY = this.getInteractionTopY(anchor) + ENTITY_MAX_VERTICAL_PADDING;
AABB box = new AABB(
- position.x - influence, minY,
- position.z - influence, position.x + influence,
- maxY, position.z + influence
+ anchor.x - influence, minY,
+ anchor.z - influence, anchor.x + influence,
+ maxY, anchor.z + influence
);
- // Base along-wind component (ambient)
- double ambientSpeed = Math.max(0.0, wind.gustSpeed()) * WIND_SPEED_SCALING_FACTOR;
- double ax = Math.cos(wind.angleRadians()) * ambientSpeed;
- double az = Math.sin(wind.angleRadians()) * ambientSpeed;
-
+ int eligibleEntities = 0;
for (Entity entity : serverLevel.getEntities(null, box)) {
- // Vector from entity to tornado center (horizontal)
- double dx = position.x - entity.getX();
- double dz = position.z - entity.getZ();
- double distSq = dx * dx + dz * dz;
- double dist = Math.sqrt(distSq);
-
- // Avoid div by zero; normalize inward vector
- double nx = dist > 1e-4 ? dx / dist : 0.0;
- double nz = dist > 1e-4 ? dz / dist : 0.0;
-
- // Suction strength scales with proximity, capped
- double suctionRadius = Math.max(4.0, this.getSuctionRadius());
- double proximity = Math.max(0.0, 1.0 - Math.min(dist, suctionRadius) / suctionRadius);
-
- // Tangential (swirl) component: perpendicular to inward vector
- double swirlDirX = -nz;
- double swirlDirZ = nx;
-
- // Scale factors — stronger than before; scales with tornado level
- double baseMag = 0.06 + this.getLevel().getMaxWindSpeed() * 0.02; // stronger baseline
- double suctionMag = baseMag * 2.0 * proximity; // inward
- double swirlMag = baseMag * 1.5 * Math.sqrt(proximity); // rotational swirl
-
- double vx = ax + nx * suctionMag + swirlDirX * swirlMag;
- double vz = az + nz * suctionMag + swirlDirZ * swirlMag;
-
- // Vertical lift increases near center
- double vy = 0.02 * proximity * (1.0 + this.getLevel().getMaxWindSpeed() * 0.02);
-
- entity.push(vx, vy, vz);
- if (entity instanceof Player) {
- entity.hurtMarked = true;
+ if (!this.isAffectedEntity(serverLevel, entity)) {
+ continue;
}
+ eligibleEntities++;
+ this.applyTornadoForces(serverLevel, entity, anchor);
}
+ this.debugEligibleEntityCount = eligibleEntities;
+ this.debugCapturedEntityCount = this.capturedEntities.size();
}
- // worker thread seulement
- private void demolishBlocks(ServerLevel level) {
- final BlockPos center = BlockPos.containing(position);
- final int intRadius = Mth.ceil(radius);
- final double outerSq = (radius + 5) * (radius + 5);
- final double innerSq = radius * radius;
- final double band = Math.max(1.0, outerSq - innerSq);
- final double invBand = 1.0 / band;
- final BlockPos min = center.offset(-intRadius - DEBRIS_RANGE_EXTENSION, 0, -intRadius - DEBRIS_RANGE_EXTENSION);
- final BlockPos max = center.offset( intRadius + DEBRIS_RANGE_EXTENSION, 3 + intRadius, intRadius + DEBRIS_RANGE_EXTENSION);
-
- RandomSource random = RandomSource.create();
- it.unimi.dsi.fastutil.longs.LongArrayList toDestroy = new it.unimi.dsi.fastutil.longs.LongArrayList(2048);
- it.unimi.dsi.fastutil.longs.LongArrayList toDestroyGlass = new it.unimi.dsi.fastutil.longs.LongArrayList(2048);
-
- // lecture off thread avec checks stricts
- for (BlockPos pos : BlockPos.betweenClosed(min, max)) {
- // ne charge pas de chunk ici
- if (!level.isLoaded(pos)) continue;
-
- try {
- // récupère le chunk si déjà chargé sinon skip
- LevelChunk chunk = level.getChunkSource().getChunk(pos.getX() >> 4, pos.getZ() >> 4, false);
- if (chunk == null) continue;
-
- // lecture état depuis le chunk existant
- BlockState state = chunk.getBlockState(pos);
- if (state.isAir()) continue;
- final double distSq = pos.distSqr(center);
- // ton test demandé hors main thread
- if (state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS)) {
- toDestroy.add(pos.asLong());
+ private boolean demolishBlocks(ServerLevel level) {
+ Vec3 anchor = this.getInteractionAnchor(level);
+ BlockPos center = BlockPos.containing(anchor);
+ float stormFactor = StormSeverityScale.toNormalized(this.stormLevel);
+ float windfieldWidth = this.getWindfieldWidth();
+ float destructionRadius = (float) Math.max(this.getCaptureRadius() * 0.88D, this.radius * (2.35F + stormFactor * 0.60F));
+ float coreRadius = (float) Math.max(this.getCoreRadius(), this.radius * (1.10F + stormFactor * 0.18F));
+ this.debugDestructionSweepRadius = destructionRadius;
+ int intRadius = Mth.ceil(destructionRadius);
+ double outerSq = destructionRadius * destructionRadius;
+ RandomSource random = RandomSource.create(this.id.getLeastSignificantBits() ^ (this.ageTicks * 31L));
+
+ int scannedColumns = 0;
+ int eligibleColumns = 0;
+ int candidateBlocks = 0;
+ int leafLogDestroyed = 0;
+ int weakDestroyed = 0;
+ int grassScoured = 0;
+ int glassDestroyed = 0;
+ int[] spawnedDebrisEntities = new int[] {0};
+ int maxLeafLogBreaks = 280 + Mth.floor(this.normalizedIntensity * 420.0F + stormFactor * 320.0F);
+ int maxWeakBreaks = 220 + Mth.floor(this.normalizedIntensity * 280.0F + stormFactor * 220.0F);
+ int maxGrassScours = 96 + Mth.floor(this.normalizedIntensity * 132.0F + stormFactor * 84.0F);
+ int maxGlassBreaks = 36 + Mth.floor(this.normalizedIntensity * 64.0F + stormFactor * 44.0F);
+ int maxDebrisEntitySpawns = BASE_MAX_DEBRIS_ENTITY_SPAWNS
+ + Mth.floor(this.normalizedIntensity * 10.0F + stormFactor * 8.0F);
+ int minBuildY = level.getMinBuildHeight();
+ int maxBuildY = level.getMaxBuildHeight() - 1;
+ BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
+
+ for (int dx = -intRadius; dx <= intRadius; dx++) {
+ for (int dz = -intRadius; dz <= intRadius; dz++) {
+ double horizontalDistSq = dx * dx + dz * dz;
+ if (horizontalDistSq > outerSq) {
+ continue;
+ }
+
+ int x = center.getX() + dx;
+ int z = center.getZ() + dz;
+ cursor.set(x, Mth.floor(anchor.y), z);
+ if (!level.isLoaded(cursor)) {
+ continue;
}
- else if (AtmosphereUtils.isGlass(state)) {
- if (distSq > outerSq) continue;
+ scannedColumns++;
+ float distance = Mth.sqrt((float) horizontalDistSq);
+ float columnStrength = 1.0F - smoothStep(coreRadius, destructionRadius, distance);
+ boolean coreColumn = distance <= coreRadius;
+ if (columnStrength <= 0.06F && !coreColumn) {
+ continue;
+ }
+ eligibleColumns++;
- final float pMax = 0.35f;
- final double t = Mth.clamp((outerSq - distSq) * invBand, 0.0, 1.0);
- final float p = (float) (t * pMax);
+ int terrainY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
+ int canopyY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
+ boolean forceBreak = coreColumn || columnStrength >= 0.72F;
+ int sweepStartY = Math.max(minBuildY, terrainY);
+ int sweepEndY = Math.min(maxBuildY, Math.max(canopyY + 8, terrainY + 12 + Mth.floor(columnStrength * 14.0F)));
+ for (int y = sweepStartY; y <= sweepEndY; y++) {
+ if (weakDestroyed >= maxWeakBreaks && grassScoured >= maxGrassScours && glassDestroyed >= maxGlassBreaks) {
+ break;
+ }
- if (random.nextFloat() < p) {
- toDestroyGlass.add(pos.asLong());
+ cursor.set(x, y, z);
+ if (!level.isLoaded(cursor) || StormShieldManager.isProtected(level, cursor)) {
+ continue;
+ }
+
+ BlockState state = level.getBlockState(cursor);
+ if (state.isAir() || !state.getFluidState().isEmpty()) {
+ continue;
+ }
+ candidateBlocks++;
+
+ if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) {
+ if (leafLogDestroyed < maxLeafLogBreaks) {
+ float breakChance = Mth.clamp(
+ 0.44F + columnStrength * 0.46F + this.normalizedIntensity * 0.24F + stormFactor * 0.16F,
+ 0.0F,
+ 1.0F
+ );
+ leafLogDestroyed += this.destroyTreeClusterImmediate(
+ level,
+ cursor.immutable(),
+ random,
+ breakChance,
+ forceBreak,
+ maxLeafLogBreaks - leafLogDestroyed,
+ anchor,
+ spawnedDebrisEntities,
+ maxDebrisEntitySpawns
+ );
+ }
+ continue;
+ }
+
+ if (AtmosphereUtils.isGlass(state)) {
+ if (glassDestroyed >= maxGlassBreaks) {
+ continue;
+ }
+ float glassChance = Mth.clamp(
+ 0.24F + columnStrength * 0.56F + this.normalizedIntensity * 0.22F + stormFactor * 0.18F,
+ 0.0F,
+ 1.0F
+ );
+ if (forceBreak || random.nextFloat() < glassChance) {
+ if (this.removeBlockWithDebris(
+ level,
+ cursor.immutable(),
+ state,
+ anchor,
+ random,
+ spawnedDebrisEntities,
+ maxDebrisEntitySpawns,
+ 0.10F
+ )) {
+ glassDestroyed++;
+ }
+ }
+ continue;
+ }
+
+ if (isSurfaceSoilBlock(state)) {
+ if (grassScoured >= maxGrassScours) {
+ continue;
+ }
+ float scourChance = Mth.clamp(
+ 0.26F + columnStrength * 0.44F + this.normalizedIntensity * 0.14F + stormFactor * 0.10F,
+ 0.0F,
+ 1.0F
+ );
+ boolean extremeExcavation = forceBreak
+ && columnStrength >= 0.96F
+ && this.normalizedIntensity >= 0.96F
+ && stormFactor >= 0.85F
+ && random.nextFloat() < (state.is(Blocks.DIRT) ? 0.025F : 0.055F);
+ if (extremeExcavation) {
+ if (this.removeBlockWithDebris(
+ level,
+ cursor.immutable(),
+ state,
+ anchor,
+ random,
+ spawnedDebrisEntities,
+ maxDebrisEntitySpawns,
+ 0.28F
+ )) {
+ weakDestroyed++;
+ }
+ } else if (!state.is(Blocks.DIRT) && (forceBreak || random.nextFloat() < scourChance)) {
+ level.setBlockAndUpdate(cursor, Blocks.DIRT.defaultBlockState());
+ grassScoured++;
+ }
+ continue;
}
- }
+ boolean looseTerrain = isLooseTerrainBlock(state);
+ boolean weakBlock = isWeakBlock(state, level, cursor, stormFactor);
+ boolean eligibleWeak = isVegetationBlock(state) || isSimpleStructureBlock(state) || looseTerrain || weakBlock;
+ if (!eligibleWeak || weakDestroyed >= maxWeakBreaks) {
+ continue;
+ }
- } catch (Throwable t) {
- // au moindre souci on ignore cette position
+ float breakChance = Mth.clamp(
+ 0.20F + columnStrength * 0.54F + this.normalizedIntensity * 0.24F + stormFactor * 0.20F,
+ 0.0F,
+ 1.0F
+ );
+ if (looseTerrain) {
+ breakChance *= 1.25F;
+ }
+ if (isVegetationBlock(state)) {
+ breakChance *= 1.18F;
+ }
+ if (forceBreak || random.nextFloat() < breakChance) {
+ if (this.removeBlockWithDebris(
+ level,
+ cursor.immutable(),
+ state,
+ anchor,
+ random,
+ spawnedDebrisEntities,
+ maxDebrisEntitySpawns,
+ this.getDebrisSpawnChance(state)
+ )) {
+ weakDestroyed++;
+ }
+ }
+ }
}
}
- // destruction uniquement sur le thread serveur
- final int perTick = 256;
- this._destroyCursor = 0;
- if (!toDestroy.isEmpty()) {
- AsyncAtmosphereService.runOnMainThread(
- ()-> processLeafLogDestruction(level, toDestroy, perTick)
- );
- }
- if (!toDestroyGlass.isEmpty()) {
- GlassDamageManager.damageGlass(level, toDestroyGlass);
- }
+ this.debugDestructionCandidateBlockCount = candidateBlocks;
+ this.debugDestroyedLeafLogCount = leafLogDestroyed;
+ this.debugDestroyedWeakCount = weakDestroyed;
+ this.debugDestroyedGrassCount = grassScoured;
+ this.debugDestroyedGlassCount = glassDestroyed;
+ this.debugDestroyedBlockCount = leafLogDestroyed + weakDestroyed + grassScoured + glassDestroyed;
+ float debrisGain = Math.min(1.0F,
+ leafLogDestroyed * 0.010F
+ + weakDestroyed * 0.018F
+ + grassScoured * 0.008F
+ + glassDestroyed * 0.024F);
+ this.recentDebrisScore = Mth.clamp(this.recentDebrisScore + debrisGain, 0.0F, 1.0F);
+
+ return leafLogDestroyed > 0 || weakDestroyed > 0 || grassScoured > 0 || glassDestroyed > 0;
}
- // curseur pour le batching
- private int _destroyCursor = 0;
+ private int destroyTreeClusterImmediate(ServerLevel level,
+ BlockPos origin,
+ RandomSource random,
+ float breakChance,
+ boolean forceBreak,
+ int remainingBudget,
+ Vec3 anchor,
+ int[] spawnedDebrisEntities,
+ int maxDebrisEntitySpawns) {
+ if (remainingBudget <= 0) {
+ return 0;
+ }
- // main thread seulement
- private void processLeafLogDestruction(ServerLevel level,
- it.unimi.dsi.fastutil.longs.LongArrayList list,
- int perTick) {
- if (_destroyCursor >= list.size()) { _destroyCursor = 0; return; }
+ int originX = origin.getX();
+ int originY = origin.getY();
+ int originZ = origin.getZ();
+ int destroyed = 0;
+ ArrayDeque queue = new ArrayDeque<>();
+ Set visited = new HashSet<>();
+ queue.add(origin);
+ visited.add(origin.asLong());
- int end = Math.min(_destroyCursor + perTick, list.size());
- for (int i = _destroyCursor; i < end; i++) {
- BlockPos pos = BlockPos.of(list.getLong(i));
- if (!level.isLoaded(pos)) continue;
+ while (!queue.isEmpty() && destroyed < remainingBudget && visited.size() <= TREE_CLUSTER_VISIT_LIMIT) {
+ BlockPos current = queue.removeFirst();
+ if (!level.isLoaded(current) || StormShieldManager.isProtected(level, current)) {
+ continue;
+ }
+
+ BlockState state = level.getBlockState(current);
+ if (!(state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES) || isVegetationBlock(state))) {
+ continue;
+ }
+
+ if (forceBreak || random.nextFloat() < breakChance) {
+ if (this.removeBlockWithDebris(
+ level,
+ current,
+ state,
+ anchor,
+ random,
+ spawnedDebrisEntities,
+ maxDebrisEntitySpawns,
+ this.getDebrisSpawnChance(state)
+ )) {
+ destroyed++;
+ }
+ }
- BlockState state = level.getBlockState(pos);
- if (!(state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS))) continue;
- level.destroyBlock(pos, false);
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dz = -1; dz <= 1; dz++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx == 0 && dy == 0 && dz == 0) {
+ continue;
+ }
+ BlockPos next = current.offset(dx, dy, dz);
+ int localX = next.getX() - originX;
+ int localY = next.getY() - originY;
+ int localZ = next.getZ() - originZ;
+ if (Math.abs(localX) > TREE_CLUSTER_HORIZONTAL_RADIUS
+ || Math.abs(localZ) > TREE_CLUSTER_HORIZONTAL_RADIUS
+ || localY < -TREE_CLUSTER_BELOW
+ || localY > TREE_CLUSTER_ABOVE) {
+ continue;
+ }
+ if (visited.add(next.asLong())) {
+ queue.addLast(next);
+ }
+ }
+ }
+ }
}
+ return destroyed;
+ }
- _destroyCursor = end;
- if (_destroyCursor < list.size()) {
- level.getServer().execute(() -> processLeafLogDestruction(level, list, perTick));
- } else {
- _destroyCursor = 0;
+ private boolean removeBlockWithDebris(ServerLevel level,
+ BlockPos pos,
+ BlockState state,
+ Vec3 anchor,
+ RandomSource random,
+ int[] spawnedDebrisEntities,
+ int maxDebrisEntitySpawns,
+ float debrisChance) {
+ float clampedDebrisChance = Mth.clamp(debrisChance, 0.0F, MAX_DEBRIS_ENTITY_SPAWN_CHANCE);
+ if (spawnedDebrisEntities[0] < maxDebrisEntitySpawns
+ && this.isVisualDebrisBlock(state)
+ && random.nextFloat() < clampedDebrisChance
+ && this.spawnVisualDebrisEntity(level, pos, state, anchor, random)) {
+ spawnedDebrisEntities[0]++;
+ return true;
}
+ return level.destroyBlock(pos, false);
}
+ private boolean spawnVisualDebrisEntity(ServerLevel level,
+ BlockPos pos,
+ BlockState state,
+ Vec3 anchor,
+ RandomSource random) {
+ if (!level.isLoaded(pos) || StormShieldManager.isProtected(level, pos) || state.isAir()) {
+ return false;
+ }
+ FallingBlockEntity debris = FallingBlockEntity.fall(level, pos, state);
+ debris.disableDrop();
+ debris.setNoGravity(true);
+ debris.time = 560 + random.nextInt(20);
+ Vec3 blockCenter = Vec3.atCenterOf(pos);
+ Vec3 towardAnchor = anchor.subtract(blockCenter);
+ double horizontalDistance = Math.max(Math.hypot(towardAnchor.x, towardAnchor.z), 0.001D);
+ Vec3 inward = new Vec3(towardAnchor.x / horizontalDistance, 0.0D, towardAnchor.z / horizontalDistance);
+ float rotationDirection = random.nextBoolean() ? 1.0F : -1.0F;
+ Vec3 tangential = new Vec3(-inward.z * rotationDirection, 0.0D, inward.x * rotationDirection);
+ double inwardStrength = 0.08D + this.normalizedIntensity * 0.12D;
+ double tangentialStrength = 0.12D + StormSeverityScale.toNormalized(this.stormLevel) * 0.10D;
+ double verticalStrength = 0.18D + this.normalizedIntensity * 0.12D + random.nextDouble() * 0.06D;
+ debris.setDeltaMovement(
+ inward.scale(inwardStrength)
+ .add(tangential.scale(tangentialStrength))
+ .add(0.0D, verticalStrength, 0.0D)
+ );
+ return true;
+ }
- private void playDemolitionSound(Level level) {
- BlockPos center = BlockPos.containing(position);
+ private float getDebrisSpawnChance(BlockState state) {
+ if (state.is(BlockTags.LOGS) || state.is(BlockTags.PLANKS)) {
+ return 0.38F;
+ }
+ if (state.is(BlockTags.LEAVES)) {
+ return 0.24F;
+ }
+ if (isSurfaceSoilBlock(state) || isLooseTerrainBlock(state)) {
+ return 0.30F;
+ }
+ if (isSimpleStructureBlock(state)) {
+ return 0.32F;
+ }
+ return 0.16F;
+ }
+ private void playDemolitionSound(Level level) {
+ BlockPos center = level instanceof ServerLevel serverLevel
+ ? BlockPos.containing(this.getInteractionAnchor(serverLevel))
+ : BlockPos.containing(this.position);
level.playLocalSound(
center.getX(), center.getY(), center.getZ(),
SoundEvents.GENERIC_EXPLODE,
SoundSource.WEATHER,
- 2.0f,
- 0.5f + level.getRandom().nextFloat() * 0.4f,
+ 1.0F + this.normalizedIntensity,
+ 0.6F + level.getRandom().nextFloat() * 0.3F,
false
);
}
+
+ private void refreshStormLevel(ServerLevel level, long gameTime) {
+ int strongest = this.cloudRegion == null ? StormSeverityScale.MIN_LEVEL : StormSeverityScale.clamp(net.Gabou.projectatmosphere.modules.core.CloudLibrary.getSeverityFromRessourceLocation(this.cloudRegion.getCloudTypeId()));
+ var regionKey = net.Gabou.projectatmosphere.util.RegionInstanceKey.from(BlockPos.containing(this.position));
+ strongest = Math.max(strongest, StormSeverityScale.resolve(level, regionKey, gameTime));
+ this.stormLevel = strongest;
+ this.targetIntensity = defaultTargetIntensity(this.maxRadius, this.wind, this.stormLevel);
+ }
+
+ @Nullable
+ private TornadoDescriptor findDescriptor() {
+ if (!(this.cloudRegion instanceof ITornadoRegion tornadoRegion)) {
+ return null;
+ }
+ return tornadoRegion.findTornado(this.id);
+ }
+
+ private static float defaultTargetIntensity(float radius, WindVector wind, int stormLevel) {
+ float radiusFactor = Mth.clamp((radius - 5.0F) / 20.0F, 0.0F, 1.0F);
+ float windFactor = Mth.clamp((wind.baseSpeed() - 12.0F) / 28.0F, 0.0F, 1.0F);
+ float stormFactor = StormSeverityScale.toNormalized(stormLevel);
+ return Mth.clamp(0.18F + radiusFactor * 0.28F + windFactor * 0.18F + stormFactor * 0.42F, 0.18F, 1.0F);
+ }
+
+ private static boolean isVegetationBlock(BlockState state) {
+ return state.is(BlockTags.LEAVES)
+ || state.is(BlockTags.SAPLINGS)
+ || state.is(BlockTags.FLOWERS)
+ || state.is(BlockTags.CROPS)
+ || state.is(Blocks.VINE)
+ || state.is(Blocks.TALL_GRASS)
+ || state.is(Blocks.GRASS)
+ || state.is(Blocks.FERN)
+ || state.is(Blocks.LARGE_FERN)
+ || state.is(Blocks.DEAD_BUSH)
+ || state.is(Blocks.SUGAR_CANE)
+ || state.is(Blocks.BAMBOO)
+ || state.is(Blocks.SWEET_BERRY_BUSH)
+ || state.is(Blocks.KELP)
+ || state.is(Blocks.KELP_PLANT)
+ || state.is(Blocks.SEAGRASS)
+ || state.is(Blocks.TALL_SEAGRASS)
+ || state.canBeReplaced();
+ }
+
+ private static boolean isSimpleStructureBlock(BlockState state) {
+ return state.is(BlockTags.PLANKS)
+ || state.is(BlockTags.WOODEN_DOORS)
+ || state.is(BlockTags.WOODEN_TRAPDOORS)
+ || state.is(BlockTags.FENCES)
+ || state.is(BlockTags.FENCE_GATES)
+ || state.is(BlockTags.WOOL)
+ || state.is(BlockTags.WOODEN_STAIRS)
+ || state.is(BlockTags.WOODEN_SLABS)
+ || state.is(BlockTags.BEDS)
+ || state.is(BlockTags.CAMPFIRES);
+ }
+
+ private static boolean isSurfaceSoilBlock(BlockState state) {
+ return state.is(Blocks.DIRT)
+ || state.is(Blocks.COARSE_DIRT)
+ || state.is(Blocks.ROOTED_DIRT)
+ || state.is(Blocks.GRASS_BLOCK)
+ || state.is(Blocks.PODZOL)
+ || state.is(Blocks.MYCELIUM)
+ || state.is(Blocks.DIRT_PATH)
+ || state.is(Blocks.FARMLAND)
+ || state.is(Blocks.MUD)
+ || state.is(Blocks.MUDDY_MANGROVE_ROOTS)
+ || state.is(Blocks.MOSS_BLOCK);
+ }
+
+ private static boolean isLooseTerrainBlock(BlockState state) {
+ return state.is(Blocks.CLAY)
+ || state.is(Blocks.SAND)
+ || state.is(Blocks.RED_SAND)
+ || state.is(Blocks.GRAVEL)
+ || state.is(Blocks.SNOW)
+ || state.is(Blocks.SNOW_BLOCK)
+ || state.is(Blocks.POWDER_SNOW);
+ }
+
+ private boolean isVisualDebrisBlock(BlockState state) {
+ return state.is(BlockTags.LOGS)
+ || state.is(BlockTags.LEAVES)
+ || state.is(BlockTags.PLANKS)
+ || state.is(BlockTags.WOODEN_SLABS)
+ || state.is(BlockTags.WOODEN_STAIRS)
+ || state.is(BlockTags.FENCES)
+ || state.is(BlockTags.FENCE_GATES)
+ || state.is(BlockTags.WOOL)
+ || isSurfaceSoilBlock(state)
+ || isLooseTerrainBlock(state);
+ }
+
+ private static boolean isWeakBlock(BlockState state, Level level, BlockPos pos, float stormFactor) {
+ if (state.is(BlockTags.BASE_STONE_OVERWORLD)
+ || state.is(BlockTags.BASE_STONE_NETHER)
+ || state.is(BlockTags.LOGS)
+ || AtmosphereUtils.isGlass(state)) {
+ return false;
+ }
+ float destroySpeed = state.getDestroySpeed(level, pos);
+ if (destroySpeed < 0.0F) {
+ return false;
+ }
+ float threshold = 1.1F + stormFactor * 1.8F;
+ return destroySpeed <= threshold;
+ }
+
+ private float getWindfieldWidth() {
+ return Math.max(this.radius * (1.65F + StormSeverityScale.toNormalized(this.stormLevel) * 0.45F), 28.0F);
+ }
+
+ private double getCoreRadius() {
+ return Math.max(this.getWindfieldWidth() * CORE_RADIUS_FACTOR, this.radius * 1.05F);
+ }
+
+ private double getCaptureRadius() {
+ return Math.max(this.getWindfieldWidth() * CAPTURE_RADIUS_FACTOR, this.getCoreRadius() + 8.0D);
+ }
+
+ private double getOuterInfluenceRadius() {
+ return Math.max(this.getWindfieldWidth() * 1.75D, this.getCaptureRadius() + OUTER_ENTITY_INFLUENCE_PADDING);
+ }
+
+ private double getInteractionTopY(Vec3 anchor) {
+ return anchor.y + Math.max(36.0D, this.visualHeight * 0.96D);
+ }
+
+ private boolean isAffectedEntity(ServerLevel level, Entity entity) {
+ if (entity == null || !entity.isAlive() || entity.isRemoved() || entity.noPhysics) {
+ return false;
+ }
+ if (entity instanceof Player player && (player.isCreative() || player.isSpectator())) {
+ return false;
+ }
+ return !StormShieldManager.isProtected(level, entity.position());
+ }
+
+ private void applyTornadoForces(ServerLevel level, Entity entity, Vec3 anchor) {
+ CapturedEntityState captured = this.capturedEntities.get(entity.getId());
+ double outerInfluenceRadius = captured != null
+ ? this.getOuterInfluenceRadius() * CAPTURE_HYSTERESIS_FACTOR
+ : this.getOuterInfluenceRadius();
+ double captureRadius = this.getCaptureRadius();
+ double coreRadius = this.getCoreRadius();
+ double topY = this.getInteractionTopY(anchor);
+ int terrainY = level.getHeight(
+ Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
+ entity.blockPosition().getX(),
+ entity.blockPosition().getZ()
+ ) - 1;
+
+ if (entity.getY() + entity.getBbHeight() < terrainY - 1.5D
+ || entity.getY() > topY + ENTITY_MAX_VERTICAL_PADDING) {
+ this.capturedEntities.remove(entity.getId());
+ return;
+ }
+
+ Vec3 relativePos = entity.position().subtract(anchor);
+ double horizontalDistance = Math.hypot(relativePos.x, relativePos.z);
+ if (horizontalDistance > outerInfluenceRadius) {
+ this.capturedEntities.remove(entity.getId());
+ return;
+ }
+
+ double safeDistance = Math.max(horizontalDistance, 0.001D);
+ Vec3 inward = new Vec3(-relativePos.x / safeDistance, 0.0D, -relativePos.z / safeDistance);
+ float rotationDirection = captured != null ? captured.rotationDirection : this.getRotationDirection(entity);
+ Vec3 tangential = new Vec3(-inward.z * rotationDirection, 0.0D, inward.x * rotationDirection);
+ float stormFactor = StormSeverityScale.toNormalized(this.stormLevel);
+ float tornadoForce = Mth.clamp(this.normalizedIntensity * 0.72F + stormFactor * 0.28F, 0.25F, 1.0F);
+ float approachFactor = 1.0F - smoothStep((float) captureRadius, (float) outerInfluenceRadius, (float) horizontalDistance);
+ float coreFactor = 1.0F - smoothStep((float) (coreRadius * 0.72D), (float) (captureRadius * 0.94D), (float) horizontalDistance);
+ float heightNorm = Mth.clamp((float) ((entity.getY() - anchor.y) / Math.max(topY - anchor.y, 1.0D)), 0.0F, 1.25F);
+ float liftWindow = 1.0F - smoothStep(0.88F, 1.05F, heightNorm);
+ boolean shouldCapture = captured != null
+ || horizontalDistance <= captureRadius
+ || (approachFactor > 0.72F && heightNorm < 1.0F);
+ if (captured == null && shouldCapture) {
+ captured = this.createCaptureState(entity, captureRadius, horizontalDistance);
+ this.capturedEntities.put(entity.getId(), captured);
+ }
+
+ Vec3 translationAssist = this.motion.scale(0.10D + tornadoForce * 0.10D);
+ Vec3 add;
+ if (captured != null) {
+ captured.lastSeenAge = this.ageTicks;
+ captured.captureTicks++;
+ float captureProgress = smoothStep(2.0F, CAPTURE_FULL_TICKS, captured.captureTicks);
+ float ascentProgress = smoothStep(10.0F, CAPTURE_ASCENT_TICKS, captured.captureTicks);
+ float desiredBand = Mth.lerp(ascentProgress, OUTER_ORBIT_RADIUS_FACTOR, INNER_ORBIT_RADIUS_FACTOR + coreFactor * 0.06F);
+ captured.orbitRadiusFactor = Mth.lerp(0.20F, captured.orbitRadiusFactor, desiredBand);
+ captured.orbitAngle += (0.16F + tornadoForce * 0.14F + captureProgress * 0.12F + coreFactor * 0.06F)
+ * captured.rotationDirection;
+
+ double desiredRadius = Math.max(coreRadius * 0.72D, captureRadius * captured.orbitRadiusFactor);
+ double liftStep = (CAPTURED_LIFT_FORCE + tornadoForce * 0.26F + ascentProgress * 0.42F + coreFactor * 0.18F)
+ * captured.liftBias
+ * liftWindow;
+ double desiredHeight = Mth.clamp(
+ entity.getY() + liftStep,
+ anchor.y + 1.5D,
+ topY + ENTITY_RELEASE_HEIGHT_PADDING
+ );
+ Vec3 orbitTarget = new Vec3(
+ Math.cos(captured.orbitAngle) * desiredRadius,
+ desiredHeight,
+ Math.sin(captured.orbitAngle) * desiredRadius
+ ).add(anchor.x, 0.0D, anchor.z);
+ Vec3 towardOrbit = orbitTarget.subtract(entity.position());
+ Vec3 horizontalOrbitOffset = new Vec3(towardOrbit.x, 0.0D, towardOrbit.z);
+ Vec3 orbitCorrection = horizontalOrbitOffset.lengthSqr() > 1.0E-4
+ ? horizontalOrbitOffset.normalize()
+ : Vec3.ZERO;
+
+ add = orbitCorrection.scale(CAPTURED_SUCTION_FORCE + tornadoForce * 0.24F + captureProgress * 0.18F)
+ .add(tangential.scale(CAPTURED_TANGENTIAL_FORCE + tornadoForce * 0.20F + ascentProgress * 0.10F))
+ .add(0.0D, liftStep, 0.0D)
+ .add(translationAssist);
+ if (horizontalDistance < coreRadius * 0.42D) {
+ add = add.add(inward.scale(-0.10D - coreFactor * 0.10D));
+ }
+ } else {
+ float suctionStrength = BASE_SUCTION_FORCE + approachFactor * (0.28F + tornadoForce * 0.18F);
+ float tangentialStrength = BASE_TANGENTIAL_FORCE + approachFactor * 0.12F + coreFactor * 0.06F;
+ float liftStrength = (BASE_LIFT_FORCE + approachFactor * 0.16F + coreFactor * 0.12F)
+ * (0.75F + tornadoForce * 0.45F)
+ * liftWindow;
+
+ add = inward.scale(suctionStrength)
+ .add(tangential.scale(tangentialStrength))
+ .add(0.0D, liftStrength, 0.0D)
+ .add(translationAssist.scale(0.60D));
+ }
+
+ if (entity.onGround()) {
+ add = add.add(0.0D, captured != null ? 0.35D : 0.18D, 0.0D);
+ }
+
+ this.recordForceSample(
+ Math.max(0.0D, add.x * inward.x + add.z * inward.z),
+ Math.max(0.0D, add.y)
+ );
+
+ Vec3 current = entity.getDeltaMovement();
+ Vec3 damped = captured != null
+ ? new Vec3(current.x * 0.54D, Math.max(current.y * 0.55D, 0.0D), current.z * 0.54D)
+ : new Vec3(current.x * 0.82D, Math.max(current.y * 0.35D, -0.02D), current.z * 0.82D);
+ entity.setDeltaMovement(damped.add(add));
+
+ if (captured != null && (entity.getY() > topY + ENTITY_RELEASE_HEIGHT_PADDING || captured.captureTicks > CAPTURE_RELEASE_TICKS)) {
+ this.capturedEntities.remove(entity.getId());
+ entity.setDeltaMovement(entity.getDeltaMovement().add(tangential.scale(0.18D + tornadoForce * 0.08D)).add(0.0D, 0.20D, 0.0D));
+ }
+ entity.hasImpulse = true;
+ entity.hurtMarked = true;
+ entity.fallDistance = 0.0F;
+ if (entity instanceof ServerPlayer serverPlayer) {
+ serverPlayer.connection.send(new ClientboundSetEntityMotionPacket(serverPlayer));
+ }
+ this.applyEntityDamage(entity, captured, approachFactor, coreFactor, liftWindow);
+ }
+
+ private CapturedEntityState createCaptureState(Entity entity, double captureRadius, double dist) {
+ double angle = Math.atan2(entity.getZ() - this.position.z, entity.getX() - this.position.x);
+ float orbitRadiusFactor = Mth.clamp((float) (dist / Math.max(captureRadius, 0.001D)), 0.42F, 0.88F);
+ float liftBias = 0.88F + (float) StormMotionModel.noise01(this.id, entity.getId() * 31L, 0.07F) * 0.28F;
+ return new CapturedEntityState(
+ angle,
+ orbitRadiusFactor,
+ this.getRotationDirection(entity),
+ liftBias,
+ this.ageTicks,
+ 0
+ );
+ }
+
+ private void applyEntityDamage(Entity entity,
+ @Nullable CapturedEntityState captured,
+ float approachFactor,
+ float coreFactor,
+ float liftWindow) {
+ if (!(entity instanceof LivingEntity living)) {
+ return;
+ }
+ if (living instanceof Player player && (player.isCreative() || player.isSpectator())) {
+ return;
+ }
+ if (this.ageTicks % ENTITY_DAMAGE_INTERVAL_TICKS != Math.floorMod(entity.getId(), ENTITY_DAMAGE_INTERVAL_TICKS)) {
+ return;
+ }
+ if (captured == null && coreFactor < 0.28F && approachFactor < 0.48F) {
+ return;
+ }
+
+ float stormFactor = StormSeverityScale.toNormalized(this.stormLevel);
+ float intensityFactor = 0.48F + this.normalizedIntensity * 0.96F + stormFactor * 0.42F;
+ float pressureFactor = captured != null ? 1.35F : 0.82F;
+ float zoneFactor = 0.34F + coreFactor * 0.92F + approachFactor * 0.38F + (1.0F - liftWindow) * 0.12F;
+ float damage = (float) (this.getDamageMultiplier() * 0.08D) * intensityFactor * pressureFactor * zoneFactor;
+ damage = Mth.clamp(damage, captured != null ? 1.5F : 0.75F, 9.0F);
+ if (damage > 0.0F) {
+ living.hurt(living.damageSources().generic(), damage);
+ }
+ }
+
+ private float getRotationDirection(Entity entity) {
+ return (((this.id.getLeastSignificantBits() ^ entity.getId()) & 1L) == 0L) ? 1.0F : -1.0F;
+ }
+
+ private void resetRuntimeDebugStats() {
+ this.debugEligibleEntityCount = 0;
+ this.debugCapturedEntityCount = this.capturedEntities.size();
+ this.debugForceSampleCount = 0;
+ this.debugPullForceSum = 0.0D;
+ this.debugUpwardForceSum = 0.0D;
+ this.debugPullForceMax = 0.0D;
+ this.debugUpwardForceMax = 0.0D;
+ this.debugDestructionSweepRadius = 0.0F;
+ this.debugDestructionCandidateBlockCount = 0;
+ this.debugDestroyedBlockCount = 0;
+ this.debugDestroyedLeafLogCount = 0;
+ this.debugDestroyedWeakCount = 0;
+ this.debugDestroyedGrassCount = 0;
+ this.debugDestroyedGlassCount = 0;
+ }
+
+ private void recordForceSample(double pullForce, double upwardForce) {
+ this.debugForceSampleCount++;
+ this.debugPullForceSum += pullForce;
+ this.debugUpwardForceSum += upwardForce;
+ this.debugPullForceMax = Math.max(this.debugPullForceMax, pullForce);
+ this.debugUpwardForceMax = Math.max(this.debugUpwardForceMax, upwardForce);
+ }
+
+ private void pruneCapturedEntities(ServerLevel level) {
+ Iterator> iterator = this.capturedEntities.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ Entity entity = level.getEntity(entry.getKey());
+ if (entity == null || !entity.isAlive()) {
+ iterator.remove();
+ continue;
+ }
+ CapturedEntityState state = entry.getValue();
+ if ((this.ageTicks - state.lastSeenAge) > 22) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private float getEffectiveWindSpeed() {
+ float composite = Mth.clamp(this.normalizedIntensity * 0.72F + StormSeverityScale.toNormalized(this.stormLevel) * 0.28F, 0.0F, 1.0F);
+ return Mth.lerp(composite, MIN_EFFECTIVE_WIND, MAX_EFFECTIVE_WIND);
+ }
+
+ private void applyWaterPenalty(float waterExposure) {
+ if (waterExposure <= WATER_PENALTY_THRESHOLD) {
+ return;
+ }
+
+ float penalty = Mth.clamp((waterExposure - WATER_PENALTY_THRESHOLD) / (1.0F - WATER_PENALTY_THRESHOLD), 0.0F, 1.0F);
+ this.targetIntensity = Math.max(0.18F, this.targetIntensity * (1.0F - penalty * 0.18F));
+ }
+
+ private Vec3 getInteractionAnchor(ServerLevel level) {
+ return new Vec3(this.position.x, this.sampleTerrainSurfaceY(level), this.position.z);
+ }
+
+ private float sampleTerrainSurfaceY(ServerLevel level) {
+ int centerX = Mth.floor(this.position.x);
+ int centerZ = Mth.floor(this.position.z);
+ int sampleOffset = Math.max(2, Mth.ceil(Math.min(this.getWindfieldWidth() * 0.18F, 10.0F)));
+
+ float highestSurface = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX, centerZ) - 1.0F;
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX + sampleOffset, centerZ) - 1.0F);
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX - sampleOffset, centerZ) - 1.0F);
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX, centerZ + sampleOffset) - 1.0F);
+ highestSurface = Math.max(highestSurface, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, centerX, centerZ - sampleOffset) - 1.0F);
+ return highestSurface;
+ }
+
+ private float sampleWaterExposure(ServerLevel level) {
+ double sampleRadius = Math.max(6.0D, this.getWindfieldWidth() * 0.45D);
+ double[][] offsets = {
+ {0.0D, 0.0D},
+ {sampleRadius, 0.0D},
+ {-sampleRadius, 0.0D},
+ {0.0D, sampleRadius},
+ {0.0D, -sampleRadius},
+ {sampleRadius * 0.7D, sampleRadius * 0.7D},
+ {sampleRadius * 0.7D, -sampleRadius * 0.7D},
+ {-sampleRadius * 0.7D, sampleRadius * 0.7D},
+ {-sampleRadius * 0.7D, -sampleRadius * 0.7D}
+ };
+
+ int waterSamples = 0;
+ for (double[] offset : offsets) {
+ int x = Mth.floor(this.position.x + offset[0]);
+ int z = Mth.floor(this.position.z + offset[1]);
+ int terrainY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z);
+ BlockPos surfacePos = new BlockPos(x, terrainY - 1, z);
+ BlockPos belowPos = surfacePos.below();
+ BlockState surface = level.getBlockState(surfacePos);
+ BlockState below = level.getBlockState(belowPos);
+ if (surface.is(Blocks.WATER)
+ || below.is(Blocks.WATER)
+ || surface.getFluidState().is(FluidTags.WATER)
+ || below.getFluidState().is(FluidTags.WATER)) {
+ waterSamples++;
+ }
+ }
+ return waterSamples / (float) offsets.length;
+ }
+
+ private static final class CapturedEntityState {
+ private double orbitAngle;
+ private float orbitRadiusFactor;
+ private final float rotationDirection;
+ private final float liftBias;
+ private int lastSeenAge;
+ private int captureTicks;
+
+ private CapturedEntityState(double orbitAngle,
+ float orbitRadiusFactor,
+ float rotationDirection,
+ float liftBias,
+ int lastSeenAge,
+ int captureTicks) {
+ this.orbitAngle = orbitAngle;
+ this.orbitRadiusFactor = orbitRadiusFactor;
+ this.rotationDirection = rotationDirection;
+ this.liftBias = liftBias;
+ this.lastSeenAge = lastSeenAge;
+ this.captureTicks = captureTicks;
+ }
+ }
+
+ public record RuntimeDebugSnapshot(
+ UUID id,
+ StormLifecyclePhase phase,
+ float normalizedIntensity,
+ int eligibleEntityCount,
+ int capturedEntityCount,
+ double averagePullForce,
+ double maxPullForce,
+ double averageUpwardForce,
+ double maxUpwardForce,
+ float destructionSweepRadius,
+ int destructionCandidateBlockCount,
+ int destroyedBlockCount,
+ int destroyedLeafLogCount,
+ int destroyedWeakCount,
+ int destroyedGrassCount,
+ int destroyedGlassCount
+ ) {
+ }
+
+ private static float smoothStep(float edge0, float edge1, float value) {
+ if (edge0 == edge1) {
+ return value < edge0 ? 0.0F : 1.0F;
+ }
+ float t = Mth.clamp((value - edge0) / (edge1 - edge0), 0.0F, 1.0F);
+ return t * t * (3.0F - 2.0F * t);
+ }
}
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoManager.java b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoManager.java
index fea90d56..9ee49003 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoManager.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoManager.java
@@ -3,66 +3,177 @@
import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudRegion;
import dev.nonamecrackers2.simpleclouds.common.world.CloudManager;
import dev.nonamecrackers2.simpleclouds.common.world.SpawnRegion;
+import net.Gabou.projectatmosphere.ProjectAtmosphere;
+import net.Gabou.projectatmosphere.api.common.cloud.region.ITornadoRegion;
+import net.Gabou.projectatmosphere.api.common.cloud.region.TornadoDescriptor;
import net.Gabou.projectatmosphere.config.AtmoCommonConfig;
+import net.Gabou.projectatmosphere.modules.core.CloudLibrary;
import net.Gabou.projectatmosphere.modules.core.WindVector;
+import net.Gabou.projectatmosphere.modules.weather.StormSeverityScale;
+import net.Gabou.projectatmosphere.modules.weather.StormShieldManager;
+import net.Gabou.projectatmosphere.manager.ForecastOrchestrator;
import net.Gabou.projectatmosphere.network.NetworkHandler;
+import net.Gabou.projectatmosphere.network.RemoveTornadoPacket;
import net.Gabou.projectatmosphere.network.SpawnTornadoPacket;
+import net.Gabou.projectatmosphere.network.SyncTornadoesPacket;
import net.minecraft.client.Minecraft;
+import net.minecraft.core.BlockPos;
+import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.network.PacketDistributor;
+import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
+import java.util.UUID;
public class TornadoManager {
- private static final List ACTIVE_TORNADOES = new ArrayList<>();
+ private static final List SERVER_TORNADOES = new ArrayList<>();
+ private static final List CLIENT_TORNADOES = new ArrayList<>();
+ private static final ResourceLocation RUNTIME_TORNADO_CONTROLLER =
+ ResourceLocation.fromNamespaceAndPath(ProjectAtmosphere.MODID, "runtime_spawn");
+ private static final float MIN_VISUAL_HEIGHT = 96.0F;
+ private static final float HEIGHT_RADIUS_FACTOR = 6.0F;
+ private static final float HEIGHT_CLOUD_PADDING = 40.0F;
+ private static final float DEFAULT_ANGULAR_SPEED = 0.05F;
+ private static final int SYNC_INTERVAL_TICKS = 5;
- private static float shaderTime = 0.0f;
+ private static float shaderTime = 0.0F;
- public static void spawn(Vec3 pos, float radius, WindVector wind, Level level) {
- SpawnRegion temporaryRegion = new SpawnRegion((int)pos.x,(int) pos.z,(int) radius);
- for (CloudRegion cloud : CloudManager.get(level).getClouds()) {
- if (cloud.intersects(temporaryRegion)) {
- if (!AtmoCommonConfig.ENABLE_TORNADOES.get()) return;
- ACTIVE_TORNADOES.add(new TornadoInstance(pos, radius, wind, cloud));
- break;
- }
+ @OnlyIn(Dist.CLIENT)
+ public static void spawnClient(UUID id, Vec3 pos, float radius, WindVector wind, float bottomY, float height) {
+ applyClientSnapshot(new TornadoSnapshot(
+ id,
+ new Vec3(pos.x, bottomY, pos.z),
+ radius,
+ bottomY,
+ height,
+ wind.baseSpeed(),
+ wind.angleRadians(),
+ wind.gustSpeed(),
+ Mth.clamp((radius - 5.0F) / 20.0F, 0.25F, 1.0F),
+ StormSeverityScale.fromNormalized(Mth.clamp((radius - 5.0F) / 20.0F, 0.25F, 1.0F)),
+ 0.0F,
+ net.Gabou.projectatmosphere.modules.weather.StormLifecyclePhase.FORMING
+ ));
+ }
+
+ public static boolean spawnServer(ServerLevel level, Vec3 pos, float radius, WindVector wind) {
+ CloudRegion cloud = findIntersectingCloud(level, pos, radius);
+ int stormLevel = deriveStormLevel(level, cloud, BlockPos.containing(pos));
+ return spawnServer(level, pos, radius, wind, stormLevel);
+ }
+
+ public static boolean spawnServer(ServerLevel level, Vec3 pos, float radius, WindVector wind, int stormLevel) {
+ CloudRegion cloud = findIntersectingCloud(level, pos, radius);
+ if (cloud == null) {
+ return false;
}
+ return spawnServerInternal(level, pos, radius, wind, stormLevel, cloud, true);
+ }
+ public static boolean spawnServerWithoutCloud(ServerLevel level, Vec3 pos, float radius, WindVector wind) {
+ int stormLevel = deriveStormLevel(level, null, BlockPos.containing(pos));
+ return spawnServerWithoutCloud(level, pos, radius, wind, stormLevel);
+ }
+ public static boolean spawnServerWithoutCloud(ServerLevel level, Vec3 pos, float radius, WindVector wind, int stormLevel) {
+ return spawnServerInternal(level, pos, radius, wind, stormLevel, null, false);
}
- @OnlyIn(Dist.CLIENT)
- public static void spawnClient(Vec3 pos, float radius, WindVector wind) {
- if (!AtmoCommonConfig.ENABLE_TORNADOES.get()) return;
- Level level = Minecraft.getInstance().level;
- if (level == null) return;
- spawn(pos, radius, wind, level);
+
+ public static boolean forceSpawnServerWithoutCloud(ServerLevel level, Vec3 pos, float radius, WindVector wind) {
+ int stormLevel = deriveStormLevel(level, null, BlockPos.containing(pos));
+ return forceSpawnServerWithoutCloud(level, pos, radius, wind, stormLevel);
}
+ public static boolean forceSpawnServerWithoutCloud(ServerLevel level, Vec3 pos, float radius, WindVector wind, int stormLevel) {
+ return spawnServerInternal(level, pos, radius, wind, stormLevel, null, false, true);
+ }
-// public static void spawn(Vec3 pos, float radius) {
-// spawn(pos, radius, WindVector.fromBase(0, 0));
-// }
+ private static boolean spawnServerInternal(ServerLevel level, Vec3 pos, float radius, WindVector wind,
+ int stormLevel, @Nullable CloudRegion cloud,
+ boolean requiresCloudAttachment) {
+ return spawnServerInternal(level, pos, radius, wind, stormLevel, cloud, requiresCloudAttachment, false);
+ }
- public static void spawnServer(ServerLevel level, Vec3 pos, float radius, WindVector wind) {
- spawn(pos, radius, wind, level);
- NetworkHandler.CHANNEL.send(PacketDistributor.ALL.noArg(), new SpawnTornadoPacket(pos, radius, wind));
+ private static boolean spawnServerInternal(ServerLevel level, Vec3 pos, float radius, WindVector wind,
+ int stormLevel, @Nullable CloudRegion cloud,
+ boolean requiresCloudAttachment, boolean bypassGuards) {
+ if (!bypassGuards && !AtmoCommonConfig.ENABLE_TORNADOES.get()) {
+ return false;
+ }
+ if (!bypassGuards && StormShieldManager.isProtected(level, pos)) {
+ return false;
+ }
+
+ UUID id = UUID.randomUUID();
+ TornadoGeometry geometry = computeGeometry(level, pos, radius);
+ Vec3 spawnPos = new Vec3(pos.x, geometry.bottomY(), pos.z);
+ TornadoInstance tornado = new TornadoInstance(
+ id,
+ spawnPos,
+ radius,
+ wind,
+ DEFAULT_ANGULAR_SPEED,
+ geometry.bottomY(),
+ geometry.height(),
+ cloud,
+ stormLevel,
+ requiresCloudAttachment
+ );
+ if (!requiresCloudAttachment) {
+ tornado.activateImmediately();
+ }
+ if (cloud != null) {
+ attachDescriptor(cloud, tornado);
+ }
+ SERVER_TORNADOES.add(tornado);
+
+ NetworkHandler.CHANNEL.send(
+ PacketDistributor.ALL.noArg(),
+ new SpawnTornadoPacket(id, spawnPos, radius, wind, geometry.bottomY(), geometry.height())
+ );
+ broadcastSnapshots();
+ return true;
}
public static List getActiveTornadoes() {
- return ACTIVE_TORNADOES;
+ return SERVER_TORNADOES;
+ }
+
+ public static List getClientTornadoes() {
+ return CLIENT_TORNADOES;
}
public static void removeTornado(TornadoInstance tornado) {
- ACTIVE_TORNADOES.remove(tornado);
+ if (SERVER_TORNADOES.remove(tornado)) {
+ removeAttachedDescriptor(tornado);
+ broadcastRemoval(tornado.getId());
+ broadcastSnapshots();
+ }
}
public static void clearTornadoes() {
- ACTIVE_TORNADOES.clear();
+ for (TornadoInstance tornado : new ArrayList<>(SERVER_TORNADOES)) {
+ removeAttachedDescriptor(tornado);
+ broadcastRemoval(tornado.getId());
+ }
+ SERVER_TORNADOES.clear();
+ broadcastSnapshots();
+ }
+
+ public static void removeClientTornado(UUID id) {
+ CLIENT_TORNADOES.removeIf(tornado -> tornado.getId().equals(id));
+ }
+
+ public static void clearClientTornadoes() {
+ CLIENT_TORNADOES.clear();
}
@OnlyIn(Dist.CLIENT)
@@ -71,18 +182,193 @@ public static float getShaderTime() {
}
public static void tick(Level level) {
- ACTIVE_TORNADOES.removeIf(tornado -> tornado.getLifetimeSeconds() > 600);
- for (TornadoInstance tornado : ACTIVE_TORNADOES) {
- float speed = tornado.wind.baseSpeed() * 0.2f;
- tornado.position = tornado.position.add(
- Math.cos(tornado.wind.angleRadians()) * speed,
- 0,
- Math.sin(tornado.wind.angleRadians()) * speed);
- tornado.tick(level);
+ if (level == null) {
+ return;
}
+
if (level.isClientSide) {
- shaderTime += 0.05f;
+ shaderTime += 0.05F;
+ for (TornadoInstance tornado : CLIENT_TORNADOES) {
+ tornado.tickClient();
+ }
+ return;
+ }
+
+ ServerLevel serverLevel = (ServerLevel) level;
+ long gameTime = serverLevel.getGameTime();
+ Iterator iterator = SERVER_TORNADOES.iterator();
+ while (iterator.hasNext()) {
+ TornadoInstance tornado = iterator.next();
+ CloudRegion currentRegion = findIntersectingCloud(serverLevel, tornado.position, tornado.radius);
+ if (currentRegion != tornado.getCloudRegion()) {
+ removeAttachedDescriptor(tornado);
+ tornado.setCloudRegion(currentRegion);
+ if (currentRegion != null) {
+ attachDescriptor(currentRegion, tornado);
+ }
+ } else if (currentRegion != null) {
+ ensureDescriptor(currentRegion, tornado);
+ }
+
+ tornado.updateCloudAttachment(tornado.getCloudRegion() != null);
+
+ tornado.tickServer(serverLevel, gameTime);
+ if (tornado.isDead()) {
+ removeAttachedDescriptor(tornado);
+ iterator.remove();
+ broadcastRemoval(tornado.getId());
+ }
+ }
+
+ if (gameTime % SYNC_INTERVAL_TICKS == 0L) {
+ broadcastSnapshots();
+ }
+ }
+
+ @OnlyIn(Dist.CLIENT)
+ public static void applyClientSnapshots(List snapshots) {
+ List next = new ArrayList<>(snapshots.size());
+ for (TornadoSnapshot snapshot : snapshots) {
+ TornadoInstance existing = findClient(snapshot.id());
+ CloudRegion cloud = findClientCloud(snapshot.position(), snapshot.radius());
+ if (existing == null) {
+ existing = new TornadoInstance(
+ snapshot.id(),
+ snapshot.position(),
+ snapshot.radius(),
+ new WindVector(snapshot.windSpeed(), snapshot.windAngle(), snapshot.windGust()),
+ DEFAULT_ANGULAR_SPEED,
+ snapshot.visualBottomY(),
+ snapshot.visualHeight(),
+ cloud,
+ snapshot.stormLevel()
+ );
+ }
+ existing.applySnapshot(snapshot, cloud);
+ next.add(existing);
}
+ CLIENT_TORNADOES.clear();
+ CLIENT_TORNADOES.addAll(next);
}
+ @OnlyIn(Dist.CLIENT)
+ private static void applyClientSnapshot(TornadoSnapshot snapshot) {
+ TornadoInstance existing = findClient(snapshot.id());
+ CloudRegion cloud = findClientCloud(snapshot.position(), snapshot.radius());
+ if (existing == null) {
+ existing = new TornadoInstance(
+ snapshot.id(),
+ snapshot.position(),
+ snapshot.radius(),
+ new WindVector(snapshot.windSpeed(), snapshot.windAngle(), snapshot.windGust()),
+ DEFAULT_ANGULAR_SPEED,
+ snapshot.visualBottomY(),
+ snapshot.visualHeight(),
+ cloud,
+ snapshot.stormLevel()
+ );
+ CLIENT_TORNADOES.add(existing);
+ }
+ existing.applySnapshot(snapshot, cloud);
+ }
+
+ @OnlyIn(Dist.CLIENT)
+ private static TornadoInstance findClient(UUID id) {
+ for (TornadoInstance tornado : CLIENT_TORNADOES) {
+ if (tornado.getId().equals(id)) {
+ return tornado;
+ }
+ }
+ return null;
+ }
+
+ @OnlyIn(Dist.CLIENT)
+ @Nullable
+ private static CloudRegion findClientCloud(Vec3 pos, float radius) {
+ Level level = Minecraft.getInstance().level;
+ if (level == null) {
+ return null;
+ }
+ return findIntersectingCloud(level, pos, radius);
+ }
+
+ @Nullable
+ private static CloudRegion findIntersectingCloud(Level level, Vec3 pos, float radius) {
+ SpawnRegion temporaryRegion = new SpawnRegion(Mth.floor(pos.x), Mth.floor(pos.z), Mth.ceil(radius));
+ for (CloudRegion cloud : CloudManager.get(level).getClouds()) {
+ if (cloud.intersects(temporaryRegion)) {
+ return cloud;
+ }
+ }
+ return null;
+ }
+
+ private static TornadoGeometry computeGeometry(Level level, Vec3 pos, float radius) {
+ float bottomY = (float) pos.y;
+ float cloudBase = CloudManager.get(level).getCloudHeight();
+ float reachToCloudBase = Math.max(0.0F, cloudBase - bottomY);
+ float height = Math.max(MIN_VISUAL_HEIGHT, reachToCloudBase + radius * HEIGHT_RADIUS_FACTOR + HEIGHT_CLOUD_PADDING);
+ return new TornadoGeometry(bottomY, height);
+ }
+
+ private static void attachDescriptor(CloudRegion cloud, TornadoInstance tornado) {
+ if (cloud instanceof ITornadoRegion tornadoRegion) {
+ tornadoRegion.replaceTornado(createRuntimeDescriptor(tornado, cloud));
+ }
+ }
+
+ private static void ensureDescriptor(CloudRegion cloud, TornadoInstance tornado) {
+ if (cloud instanceof ITornadoRegion tornadoRegion && tornadoRegion.findTornado(tornado.getId()) == null) {
+ tornadoRegion.addTornado(createRuntimeDescriptor(tornado, cloud));
+ }
+ }
+
+ private static TornadoDescriptor createRuntimeDescriptor(TornadoInstance tornado, CloudRegion cloud) {
+ float offsetX = (float) (tornado.position.x - cloud.getWorldX());
+ float offsetZ = (float) (tornado.position.z - cloud.getWorldZ());
+ return new TornadoDescriptor(
+ tornado.getId(),
+ RUNTIME_TORNADO_CONTROLLER,
+ offsetX,
+ offsetZ,
+ tornado.wind.baseSpeed() * (float) Math.cos(tornado.wind.angleRadians()) * 0.05F,
+ tornado.wind.baseSpeed() * (float) Math.sin(tornado.wind.angleRadians()) * 0.05F,
+ tornado.radius,
+ tornado.getVisualBottomY(),
+ tornado.getVisualHeight()
+ );
+ }
+
+ private static void removeAttachedDescriptor(TornadoInstance tornado) {
+ if (tornado.getCloudRegion() instanceof ITornadoRegion tornadoRegion) {
+ tornadoRegion.removeTornado(tornado.getId());
+ }
+ }
+
+ private static void broadcastRemoval(UUID id) {
+ NetworkHandler.CHANNEL.send(PacketDistributor.ALL.noArg(), new RemoveTornadoPacket(id));
+ }
+
+ private static void broadcastSnapshots() {
+ List snapshots = new ArrayList<>(SERVER_TORNADOES.size());
+ for (TornadoInstance tornado : SERVER_TORNADOES) {
+ snapshots.add(tornado.snapshot());
+ }
+ NetworkHandler.CHANNEL.send(PacketDistributor.ALL.noArg(), new SyncTornadoesPacket(snapshots));
+ }
+
+ private record TornadoGeometry(float bottomY, float height) {
+ }
+
+ private static int deriveStormLevel(ServerLevel level, @Nullable CloudRegion cloud, net.minecraft.core.BlockPos pos) {
+ int cloudLevel = cloud == null ? StormSeverityScale.MIN_LEVEL : CloudLibrary.getSeverityFromRessourceLocation(cloud.getCloudTypeId());
+ RegionLevelProbe probe = RegionLevelProbe.at(level, pos);
+ return Math.max(cloudLevel, probe.level());
+ }
+
+ private record RegionLevelProbe(int level) {
+ private static RegionLevelProbe at(ServerLevel level, net.minecraft.core.BlockPos pos) {
+ return new RegionLevelProbe(StormSeverityScale.resolve(level, net.Gabou.projectatmosphere.util.RegionInstanceKey.from(pos), level.getGameTime()));
+ }
+ }
}
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoProbabilityManager.java b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoProbabilityManager.java
index b23e83e3..645902c4 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoProbabilityManager.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoProbabilityManager.java
@@ -10,6 +10,8 @@
import net.Gabou.projectatmosphere.data.TornadoStorageManager;
import net.Gabou.projectatmosphere.manager.ForecastOrchestrator;
import net.Gabou.projectatmosphere.modules.core.CloudLibrary;
+import net.Gabou.projectatmosphere.modules.weather.RegionalWeatherPhase;
+import net.Gabou.projectatmosphere.modules.weather.StormSeverityScale;
import net.Gabou.projectatmosphere.modules.atmosphere.AtmosphericStateRegistry;
import net.Gabou.projectatmosphere.util.BiomeInstanceKey;
import net.Gabou.projectatmosphere.util.RegionInstanceKey;
@@ -35,15 +37,17 @@ public static void onScheduledCheck(ServerLevel level) {
if (isCellOnCooldown(key, level, now)) continue;
float risk = computeRisk(key, level, now);
+ int stormLevel = StormSeverityScale.resolve(level, key, now);
float riskMin = AtmoCommonConfig.TORNADO_RISK_MIN_TO_CONSIDER.get().floatValue();
if (risk < riskMin) continue;
- float chance = AtmoCommonConfig.TORNADO_BASE_TRIGGER_CHANCE.get().floatValue() * risk;
+ float chance = AtmoCommonConfig.TORNADO_BASE_TRIGGER_CHANCE.get().floatValue() * risk * (0.55F + StormSeverityScale.toNormalized(stormLevel) * 0.75F);
if (random.nextFloat() < chance) {
float intensity = map(risk,
riskMin,
riskMin + 4f,
AtmoCommonConfig.TORNADO_INTENSITY_MIN.get().floatValue(),
AtmoCommonConfig.TORNADO_INTENSITY_MAX.get().floatValue());
+ intensity = Math.max(intensity, 0.18F + StormSeverityScale.toNormalized(stormLevel) * 0.52F);
TornadoSpawner.spawn(key, level, clamp01(intensity));
TornadoStorageManager.setCooldown(key,
now + minutesToTicks(AtmoCommonConfig.TORNADO_CELL_COOLDOWN_MINUTES.get()));
@@ -109,6 +113,13 @@ public static boolean isCellOnCooldown(BiomeInstanceKey key, ServerLevel level,
}
private static boolean isStormy(RegionInstanceKey key, ServerLevel level) {
+ RegionalWeatherPhase phase = ForecastOrchestrator.getWeatherPhase(level, key, level.getGameTime());
+ if (!phase.isStormCapable()) {
+ return false;
+ }
+ if (StormSeverityScale.resolve(level, key, level.getGameTime()) < 6) {
+ return false;
+ }
ServerCloudManager manager = (ServerCloudManager) CloudManager.get(level);
CloudGenerator generator = manager.getCloudGenerator();
BlockPos pos = key.center();
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoSnapshot.java b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoSnapshot.java
new file mode 100644
index 00000000..68bb558c
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoSnapshot.java
@@ -0,0 +1,55 @@
+package net.Gabou.projectatmosphere.modules.tornado;
+
+import net.Gabou.projectatmosphere.modules.weather.StormLifecyclePhase;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.world.phys.Vec3;
+
+import java.util.UUID;
+
+public record TornadoSnapshot(
+ UUID id,
+ Vec3 position,
+ float radius,
+ float visualBottomY,
+ float visualHeight,
+ float windSpeed,
+ float windAngle,
+ float windGust,
+ float normalizedIntensity,
+ int stormLevel,
+ float recentDebrisScore,
+ StormLifecyclePhase phase
+) {
+ public void write(FriendlyByteBuf buf) {
+ buf.writeUUID(this.id);
+ buf.writeDouble(this.position.x);
+ buf.writeDouble(this.position.y);
+ buf.writeDouble(this.position.z);
+ buf.writeFloat(this.radius);
+ buf.writeFloat(this.visualBottomY);
+ buf.writeFloat(this.visualHeight);
+ buf.writeFloat(this.windSpeed);
+ buf.writeFloat(this.windAngle);
+ buf.writeFloat(this.windGust);
+ buf.writeFloat(this.normalizedIntensity);
+ buf.writeVarInt(this.stormLevel);
+ buf.writeFloat(this.recentDebrisScore);
+ buf.writeEnum(this.phase);
+ }
+
+ public static TornadoSnapshot read(FriendlyByteBuf buf) {
+ UUID id = buf.readUUID();
+ Vec3 position = new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble());
+ float radius = buf.readFloat();
+ float bottomY = buf.readFloat();
+ float height = buf.readFloat();
+ float windSpeed = buf.readFloat();
+ float windAngle = buf.readFloat();
+ float windGust = buf.readFloat();
+ float normalizedIntensity = buf.readFloat();
+ int stormLevel = buf.readVarInt();
+ float recentDebrisScore = buf.readFloat();
+ StormLifecyclePhase phase = buf.readEnum(StormLifecyclePhase.class);
+ return new TornadoSnapshot(id, position, radius, bottomY, height, windSpeed, windAngle, windGust, normalizedIntensity, stormLevel, recentDebrisScore, phase);
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoSpawner.java b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoSpawner.java
index c8e49b81..4cc19bd7 100644
--- a/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoSpawner.java
+++ b/src/main/java/net/Gabou/projectatmosphere/modules/tornado/TornadoSpawner.java
@@ -2,11 +2,16 @@
import net.Gabou.projectatmosphere.api.WindVectorApi;
import net.Gabou.projectatmosphere.config.AtmoCommonConfig;
+import net.Gabou.projectatmosphere.modules.weather.StormSeverityScale;
import net.Gabou.projectatmosphere.util.BiomeInstanceKey;
import net.Gabou.projectatmosphere.util.RegionInstanceKey;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
+import net.minecraft.tags.FluidTags;
import net.minecraft.util.RandomSource;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;
public final class TornadoSpawner {
@@ -16,22 +21,24 @@ public static void spawn(BiomeInstanceKey key, ServerLevel level, float intensit
float radiusSetting = AtmoCommonConfig.TORNADO_BASE_SPAWN_RADIUS_M.get().floatValue();
BlockPos center = pickSpawnPosNear(key, level, radiusSetting);
WindVectorApi.WindSample wind = WindVectorApi.getSurface(key);
- float radius = 5f + 20f * intensity;
+ int stormLevel = StormSeverityScale.resolve(level, net.Gabou.projectatmosphere.modules.atmosphere.AtmosphericStateRegistry.resolveRegionKey(key), level.getGameTime());
+ float radius = 5f + 18f * intensity + stormLevel * 1.5F;
net.Gabou.projectatmosphere.modules.core.WindVector w =
net.Gabou.projectatmosphere.modules.core.WindVector.fromBase(wind.speedMps(),
(float) Math.toRadians(wind.directionDeg()));
- TornadoManager.spawnServer(level, Vec3.atCenterOf(center), radius, w);
+ TornadoManager.spawnServer(level, Vec3.atCenterOf(center), radius, w, stormLevel);
}
public static void spawn(RegionInstanceKey key, ServerLevel level, float intensity) {
float radiusSetting = AtmoCommonConfig.TORNADO_BASE_SPAWN_RADIUS_M.get().floatValue();
BlockPos center = pickSpawnPosNear(key, level, radiusSetting);
WindVectorApi.WindSample wind = WindVectorApi.getSurface(key, level.getGameTime());
- float radius = 5f + 20f * intensity;
+ int stormLevel = StormSeverityScale.resolve(level, key, level.getGameTime());
+ float radius = 5f + 18f * intensity + stormLevel * 1.5F;
net.Gabou.projectatmosphere.modules.core.WindVector w =
net.Gabou.projectatmosphere.modules.core.WindVector.fromBase(wind.speedMps(),
(float) Math.toRadians(wind.directionDeg()));
- TornadoManager.spawnServer(level, Vec3.atCenterOf(center), radius, w);
+ TornadoManager.spawnServer(level, Vec3.atCenterOf(center), radius, w, stormLevel);
}
private static BlockPos pickSpawnPosNear(BiomeInstanceKey key, ServerLevel level, float radius) {
@@ -46,11 +53,70 @@ private static BlockPos pickSpawnPosNear(RegionInstanceKey key, ServerLevel leve
private static BlockPos pickSpawnPosNear(BlockPos base, ServerLevel level, float radius) {
int r = (int) radius;
RandomSource random = RandomSource.create();
- int dx = random.nextInt(-r, r + 1);
- int dz = random.nextInt(-r, r + 1);
- int y = level.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING,
- base.getX() + dx, base.getZ() + dz);
- return new BlockPos(base.getX() + dx, y, base.getZ() + dz);
+ BlockPos best = null;
+ float bestScore = Float.NEGATIVE_INFINITY;
+ for (int attempt = 0; attempt < 10; attempt++) {
+ int dx = random.nextInt(-r, r + 1);
+ int dz = random.nextInt(-r, r + 1);
+ int x = base.getX() + dx;
+ int z = base.getZ() + dz;
+ int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
+ BlockPos sample = new BlockPos(x, y, z);
+ float score = scoreSpawnSurface(level, sample);
+ if (score > bestScore) {
+ bestScore = score;
+ best = sample;
+ }
+ }
+ return best == null ? base : best;
+ }
+
+ private static float scoreSpawnSurface(ServerLevel level, BlockPos pos) {
+ BlockPos groundPos = pos.below();
+ BlockState ground = level.getBlockState(groundPos);
+ BlockState above = level.getBlockState(pos);
+ float score = 1.0F;
+ if (!ground.getFluidState().isEmpty()
+ || ground.is(Blocks.WATER)
+ || above.is(Blocks.WATER)
+ || ground.getFluidState().is(FluidTags.WATER)
+ || above.getFluidState().is(FluidTags.WATER)) {
+ score -= 8.0F;
+ }
+ if (ground.is(Blocks.GRASS_BLOCK) || ground.is(Blocks.DIRT) || ground.is(Blocks.COARSE_DIRT) || ground.is(Blocks.PODZOL)) {
+ score += 0.45F;
+ }
+ if (ground.is(Blocks.SAND) || ground.is(Blocks.RED_SAND)) {
+ score -= 0.20F;
+ }
+ score -= sampleNearbyWaterPenalty(level, pos);
+ return score;
+ }
+
+ private static float sampleNearbyWaterPenalty(ServerLevel level, BlockPos pos) {
+ int[][] offsets = {
+ {0, 0},
+ {8, 0},
+ {-8, 0},
+ {0, 8},
+ {0, -8},
+ {8, 8},
+ {8, -8},
+ {-8, 8},
+ {-8, -8}
+ };
+ int waterSamples = 0;
+ for (int[] offset : offsets) {
+ int x = pos.getX() + offset[0];
+ int z = pos.getZ() + offset[1];
+ int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z);
+ BlockPos sample = new BlockPos(x, y - 1, z);
+ BlockState state = level.getBlockState(sample);
+ if (state.is(Blocks.WATER) || state.getFluidState().is(FluidTags.WATER)) {
+ waterSamples++;
+ }
+ }
+ return waterSamples / (float) offsets.length * 5.0F;
}
}
diff --git a/src/main/java/net/Gabou/projectatmosphere/network/NetworkHandler.java b/src/main/java/net/Gabou/projectatmosphere/network/NetworkHandler.java
index 8e1b8881..9f6ce669 100644
--- a/src/main/java/net/Gabou/projectatmosphere/network/NetworkHandler.java
+++ b/src/main/java/net/Gabou/projectatmosphere/network/NetworkHandler.java
@@ -7,7 +7,7 @@
import net.minecraftforge.network.simple.SimpleChannel;
public class NetworkHandler {
- private static final String PROTOCOL_VERSION = "4";
+ private static final String PROTOCOL_VERSION = "5";
public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel(
ResourceLocation.fromNamespaceAndPath(ProjectAtmosphere.MODID, "main"),
() -> PROTOCOL_VERSION,
@@ -23,6 +23,16 @@ public static void init() {
.encoder(SpawnTornadoPacket::encode)
.consumerMainThread(SpawnTornadoPacket::handle)
.add();
+ CHANNEL.messageBuilder(RemoveTornadoPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
+ .decoder(RemoveTornadoPacket::decode)
+ .encoder(RemoveTornadoPacket::encode)
+ .consumerMainThread(RemoveTornadoPacket::handle)
+ .add();
+ CHANNEL.messageBuilder(SyncTornadoesPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
+ .decoder(SyncTornadoesPacket::decode)
+ .encoder(SyncTornadoesPacket::encode)
+ .consumerMainThread(SyncTornadoesPacket::handle)
+ .add();
CHANNEL.messageBuilder(SyncWindPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
.decoder(SyncWindPacket::decode)
.encoder(SyncWindPacket::encode)
diff --git a/src/main/java/net/Gabou/projectatmosphere/network/RemoveTornadoPacket.java b/src/main/java/net/Gabou/projectatmosphere/network/RemoveTornadoPacket.java
new file mode 100644
index 00000000..df21e03b
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/network/RemoveTornadoPacket.java
@@ -0,0 +1,34 @@
+package net.Gabou.projectatmosphere.network;
+import net.Gabou.projectatmosphere.modules.tornado.TornadoManager;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraftforge.network.NetworkEvent;
+
+import java.util.UUID;
+import java.util.function.Supplier;
+
+public class RemoveTornadoPacket {
+ private final UUID id;
+
+ public RemoveTornadoPacket(UUID id) {
+ this.id = id;
+ }
+
+ public RemoveTornadoPacket(FriendlyByteBuf buf) {
+ this.id = buf.readUUID();
+ }
+
+ public void encode(FriendlyByteBuf buf) {
+ buf.writeUUID(this.id);
+ }
+
+ public static RemoveTornadoPacket decode(FriendlyByteBuf buf) {
+ return new RemoveTornadoPacket(buf);
+ }
+
+ public void handle(Supplier ctx) {
+ ctx.get().enqueueWork(() -> {
+ TornadoManager.removeClientTornado(this.id);
+ });
+ ctx.get().setPacketHandled(true);
+ }
+}
diff --git a/src/main/java/net/Gabou/projectatmosphere/network/SpawnTornadoPacket.java b/src/main/java/net/Gabou/projectatmosphere/network/SpawnTornadoPacket.java
index 791fe2f6..d0023aa9 100644
--- a/src/main/java/net/Gabou/projectatmosphere/network/SpawnTornadoPacket.java
+++ b/src/main/java/net/Gabou/projectatmosphere/network/SpawnTornadoPacket.java
@@ -1,41 +1,53 @@
package net.Gabou.projectatmosphere.network;
-
import net.Gabou.projectatmosphere.modules.core.WindVector;
import net.Gabou.projectatmosphere.modules.tornado.TornadoManager;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.network.NetworkEvent;
+import java.util.UUID;
import java.util.function.Supplier;
public class SpawnTornadoPacket {
+ private final UUID id;
private final Vec3 pos;
private final float radius;
+ private final float bottomY;
+ private final float height;
private final float speed;
private final float angle;
private final float gust;
- public SpawnTornadoPacket(Vec3 pos, float radius, WindVector wind) {
+ public SpawnTornadoPacket(UUID id, Vec3 pos, float radius, WindVector wind, float bottomY, float height) {
+ this.id = id;
this.pos = pos;
this.radius = radius;
+ this.bottomY = bottomY;
+ this.height = height;
this.speed = wind.baseSpeed();
this.angle = wind.angleRadians();
this.gust = wind.gustSpeed();
}
public SpawnTornadoPacket(FriendlyByteBuf buf) {
+ this.id = buf.readUUID();
this.pos = new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble());
this.radius = buf.readFloat();
+ this.bottomY = buf.readFloat();
+ this.height = buf.readFloat();
this.speed = buf.readFloat();
this.angle = buf.readFloat();
this.gust = buf.readFloat();
}
public void encode(FriendlyByteBuf buf) {
+ buf.writeUUID(this.id);
buf.writeDouble(pos.x);
buf.writeDouble(pos.y);
buf.writeDouble(pos.z);
buf.writeFloat(radius);
+ buf.writeFloat(bottomY);
+ buf.writeFloat(height);
buf.writeFloat(speed);
buf.writeFloat(angle);
buf.writeFloat(gust);
@@ -47,7 +59,7 @@ public static SpawnTornadoPacket decode(FriendlyByteBuf buf) {
public void handle(Supplier ctx) {
ctx.get().enqueueWork(() -> {
- TornadoManager.spawnClient(pos, radius, new WindVector(speed, angle, gust));
+ TornadoManager.spawnClient(id, pos, radius, new WindVector(speed, angle, gust), bottomY, height);
});
ctx.get().setPacketHandled(true);
}
diff --git a/src/main/java/net/Gabou/projectatmosphere/network/SyncTornadoesPacket.java b/src/main/java/net/Gabou/projectatmosphere/network/SyncTornadoesPacket.java
new file mode 100644
index 00000000..eeeecf4e
--- /dev/null
+++ b/src/main/java/net/Gabou/projectatmosphere/network/SyncTornadoesPacket.java
@@ -0,0 +1,43 @@
+package net.Gabou.projectatmosphere.network;
+
+import net.Gabou.projectatmosphere.modules.tornado.TornadoManager;
+import net.Gabou.projectatmosphere.modules.tornado.TornadoSnapshot;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraftforge.network.NetworkEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+public class SyncTornadoesPacket {
+ private final List snapshots;
+
+ public SyncTornadoesPacket(List snapshots) {
+ this.snapshots = List.copyOf(snapshots);
+ }
+
+ public SyncTornadoesPacket(FriendlyByteBuf buf) {
+ int count = buf.readVarInt();
+ List read = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ read.add(TornadoSnapshot.read(buf));
+ }
+ this.snapshots = read;
+ }
+
+ public void encode(FriendlyByteBuf buf) {
+ buf.writeVarInt(this.snapshots.size());
+ for (TornadoSnapshot snapshot : this.snapshots) {
+ snapshot.write(buf);
+ }
+ }
+
+ public static SyncTornadoesPacket decode(FriendlyByteBuf buf) {
+ return new SyncTornadoesPacket(buf);
+ }
+
+ public void handle(Supplier ctx) {
+ ctx.get().enqueueWork(() -> TornadoManager.applyClientSnapshots(this.snapshots));
+ ctx.get().setPacketHandled(true);
+ }
+}
diff --git a/src/main/resources/assets/projectatmosphere/shaders/compute/cloud_regions.comp b/src/main/resources/assets/projectatmosphere/shaders/compute/cloud_regions.comp
index 903e7098..db00b352 100644
--- a/src/main/resources/assets/projectatmosphere/shaders/compute/cloud_regions.comp
+++ b/src/main/resources/assets/projectatmosphere/shaders/compute/cloud_regions.comp
@@ -6,259 +6,78 @@
layout(local_size_x = ${LOCAL_SIZE_X}, local_size_y = ${LOCAL_SIZE_Y}, local_size_z = ${LOCAL_SIZE_Z}) in;
struct CloudRegion {
- float posX;
- float posZ;
- float index;
- float radius;
- mat2 transform;
+ float posX;
+ float posZ;
+ float index;
+ float radius;
+ mat2 transform;
};
layout(std430) readonly buffer CloudRegions {
- CloudRegion data[];
-} cloudRegions;
+ CloudRegion data[];
+}
+cloudRegions;
layout(std430) restrict readonly buffer LodScales {
- float data[];
-} lodScales;
-
-struct CloudTornado {
- float typeIndex;
- vec2 center;
- float radius;
- float bottom;
- float height;
- vec2 padding;
-};
-
-layout(std430) readonly buffer CloudTornadoes {
- CloudTornado data[];
-} cloudTornadoes;
-
-struct CloudHurricane {
- vec4 shape0;
- vec4 shape1;
- vec4 shape2;
- vec4 shape3;
-};
-
-layout(std430) readonly buffer CloudHurricanes {
- CloudHurricane data[];
-} cloudHurricanes;
+ float data[];
+}
+lodScales;
layout(rg32f) restrict writeonly uniform image2DArray regionTexture;
uniform int TotalCloudRegions;
uniform vec2 Offset;
-bool projectatmosphere_insideTornado(vec2 coord, out float typeIndex);
-vec2 projectatmosphere_sampleHurricane(CloudHurricane hurricane, vec2 coord);
-vec2 projectatmosphere_compositeHurricane(vec2 current, vec2 hurricane);
-uniform int TotalCloudTornadoes;
-uniform int TotalCloudHurricanes;
-
-float saturate(float value)
-{
- return clamp(value, 0.0, 1.0);
-}
vec3 circle(CloudRegion region, vec2 coord)
{
- vec2 p = vec2(region.posX, region.posZ);
- coord = region.transform * (coord - p) + p;
- float d = distance(p, coord);
- float r = region.radius;
- if (d > r + 1.0 / EFF)
- return vec3(-1.0);
- else if (d < r)
- return vec3(min((r - d) * EFF, 1.0), 0.0, region.index);
- else
- return vec3(0.0, min((d - r) * EFF, 1.0), region.index);
+ vec2 p = vec2(region.posX, region.posZ);
+ coord = region.transform * (coord - p) + p;
+ float d = distance(p, coord);
+ float r = region.radius;
+ if (d > r + 1.0 / EFF)
+ return vec3(-1.0);
+ else if (d < r)
+ return vec3(min((r - d) * EFF, 1.0), 0.0, region.index);
+ else
+ return vec3(0.0, min((d - r) * EFF, 1.0), region.index);
}
vec2 composite(vec2 old, vec3 data)
{
- if (data.r > 0.0)
- {
- if (old.r >= 0.0 && old.r == data.b)
- return vec2(old.r, mix(old.g, 1.0, data.r));
- else
- return data.br;
- }
- else if (data.g >= 0.0)
- {
- if (old.r >= 0.0 && old.r == data.b)
- return old;
- else
- return vec2(old.r, old.g * data.g);
- }
- else
- {
- return old;
- }
-}
-
-bool projectatmosphere_insideTornado(vec2 coord, out float typeIndex)
-{
- for (int i = 0; i < TotalCloudTornadoes; i++)
- {
- CloudTornado tornado = cloudTornadoes.data[i];
- if (distance(coord, tornado.center) <= tornado.radius)
- {
- typeIndex = tornado.typeIndex;
- return true;
- }
- }
- return false;
-}
-
-vec2 projectatmosphere_sampleHurricane(CloudHurricane hurricane, vec2 coord)
-{
- float typeIndex = hurricane.shape0.x;
- vec2 center = hurricane.shape0.yz;
- float coreRadius = hurricane.shape1.x;
- float stormExtentRadius = hurricane.shape1.y;
- float eyeRadius = hurricane.shape1.z;
- float edgeFade = max(hurricane.shape1.w, 1.0);
-
- float bandCount = max(hurricane.shape2.x, 1.0);
- float bandWidth = max(hurricane.shape2.y, 1.0);
- float spiralTightness = hurricane.shape2.z;
- float rotationPhase = hurricane.shape2.w;
-
- float transitionStart = hurricane.shape3.y;
- float transitionEnd = max(hurricane.shape3.z, transitionStart + 1.0);
-
- vec2 local = coord - center;
- float radius = length(local);
- if (radius > stormExtentRadius + edgeFade * 1.20)
- return vec2(-1.0);
-
- float angle = atan(local.y, local.x);
- float normalizedRadius = stormExtentRadius > 0.0 ? saturate(radius / stormExtentRadius) : 0.0;
- float spinPhase = angle + max(radius - eyeRadius, 0.0) * spiralTightness - rotationPhase;
-
- float outerMask = 1.0 - smoothstep(stormExtentRadius - edgeFade * 0.34, stormExtentRadius + edgeFade * 0.92, radius);
- float eyeHole = smoothstep(eyeRadius + edgeFade * 0.06, eyeRadius + edgeFade * 0.82, radius);
-
- float eyewallCenter = eyeRadius + bandWidth * 0.36;
- float eyewallThickness = bandWidth * 0.88 + edgeFade * 0.14;
- float eyewall = 1.0 - smoothstep(eyewallThickness * 0.26, eyewallThickness, abs(radius - eyewallCenter));
- eyewall *= eyeHole;
-
- float armNoiseA = 0.5 + 0.5 * cos(spinPhase * bandCount + normalizedRadius * 7.0);
- float armNoiseB = 0.5 + 0.5 * cos(spinPhase * (bandCount * 0.72 + 1.10) - normalizedRadius * 9.5);
- float armNoise = mix(armNoiseA, armNoiseB, 0.42);
- armNoise = smoothstep(0.62, 0.94, armNoise);
-
- float spiralEnvelope = smoothstep(eyeRadius + bandWidth * 0.12, eyeRadius + bandWidth * 1.28, radius);
- spiralEnvelope *= 1.0 - smoothstep(coreRadius * 0.78, coreRadius * 1.18, radius);
-
- float innerCore = max(eyewall, armNoise * spiralEnvelope);
-
- // Start the cumulonimbus recovery close to the eyewall so the core does not hand off into a hollow ring.
- float bridgeStart = eyeRadius + bandWidth * 0.52;
- float bridgeBuildEnd = max(bridgeStart + bandWidth * 1.85, coreRadius * 0.36);
- float bridgeFadeEnd = max(coreRadius * 1.08, bridgeBuildEnd + bandWidth * 2.40);
-
- float cbBlendStart = min(transitionStart, bridgeStart);
- float cbEnvelope = smoothstep(cbBlendStart, transitionEnd, radius);
- cbEnvelope *= 1.0 - smoothstep(stormExtentRadius * 1.02, stormExtentRadius + edgeFade * 0.78, radius);
-
- float cbNoiseA = 0.5 + 0.5 * cos(angle * 1.9 - rotationPhase * 0.06 + normalizedRadius * 6.8);
- float cbNoiseB = 0.5 + 0.5 * cos(angle * 4.4 + rotationPhase * 0.03 - normalizedRadius * 12.6);
- float cbNoiseC = 0.5 + 0.5 * cos(angle * 2.7 - normalizedRadius * 4.4);
- float cbNoise = mix(mix(cbNoiseA, cbNoiseB, 0.45), cbNoiseC, 0.30);
- cbNoise = smoothstep(0.20, 0.90, cbNoise);
-
- float innerCbA = 0.5 + 0.5 * cos(spinPhase * (bandCount * 0.92 + 0.85) - normalizedRadius * 6.4);
- float innerCbB = 0.5 + 0.5 * cos(angle * 2.2 - rotationPhase * 0.10 + normalizedRadius * 4.8);
- float innerCbMask = smoothstep(0.20, 0.82, mix(innerCbA, innerCbB, 0.42));
-
- float innerBridgeEnvelope = smoothstep(bridgeStart, bridgeBuildEnd, radius);
- innerBridgeEnvelope *= 1.0 - smoothstep(coreRadius * 0.94, bridgeFadeEnd, radius);
-
- float outerBandEnvelope = smoothstep(coreRadius * 0.42, stormExtentRadius * 0.90, radius);
- outerBandEnvelope *= 1.0 - smoothstep(stormExtentRadius * 0.96, stormExtentRadius + edgeFade * 0.72, radius);
-
- float outerBandA = 0.5 + 0.5 * cos(spinPhase * (bandCount * 0.42 + 1.05) - normalizedRadius * 15.0);
- float outerBandB = 0.5 + 0.5 * cos((angle - rotationPhase * 0.16) * (bandCount * 0.30 + 1.85) + normalizedRadius * 21.0);
- outerBandA = smoothstep(0.58, 0.94, outerBandA);
- outerBandB = smoothstep(0.56, 0.92, outerBandB);
- float outerBandMask = smoothstep(0.34, 0.88, mix(outerBandA, outerBandB, 0.38));
-
- float cbMass = cbEnvelope * (0.22 + cbNoise * 0.26 + outerBandMask * 0.52);
-
- float innerBridge = innerBridgeEnvelope * (0.54 + innerCbMask * 0.26 + armNoise * 0.20);
-
- float continuityBand = smoothstep(bridgeStart, coreRadius * 0.96, radius);
- continuityBand *= 1.0 - smoothstep(coreRadius * 1.08, transitionEnd * 0.92, radius);
- continuityBand *= 0.48 + mix(armNoise, outerBandMask, 0.50) * 0.44;
-
- float spiralShoulders = outerBandEnvelope * (0.28 + outerBandMask * 0.72);
- spiralShoulders *= 0.52 + cbNoise * 0.30;
-
- float anvilEdge = smoothstep(stormExtentRadius * 0.70, stormExtentRadius * 0.95, radius);
- anvilEdge *= 1.0 - smoothstep(stormExtentRadius * 1.04, stormExtentRadius + edgeFade * 0.94, radius);
- anvilEdge *= smoothstep(0.34, 0.88, cbNoiseB) * (0.42 + outerBandMask * 0.58);
-
- float outerStorm = max(max(cbMass, spiralShoulders), max(innerBridge, continuityBand + anvilEdge * 0.20));
- float coverage = max(innerCore, outerStorm);
- coverage *= outerMask * eyeHole;
- coverage = smoothstep(0.04, 0.88, saturate(coverage));
- if (coverage <= 0.001)
- return vec2(-1.0);
-
- return vec2(typeIndex, coverage);
-}
-
-vec2 projectatmosphere_compositeHurricane(vec2 current, vec2 hurricane)
-{
- if (hurricane.x < 0.0)
- return current;
-
- if (current.r < 0.0 || current.g <= 0.0)
- return hurricane;
-
- if (current.r == hurricane.x)
- return vec2(current.r, max(current.g, hurricane.y));
-
- if (hurricane.y >= current.g)
- return hurricane;
-
- return current;
+ if (data.r > 0.0)
+ {
+ if (old.r >= 0.0 && old.r == data.b)
+ return vec2(old.r, mix(old.g, 1.0, data.r));
+ else
+ return data.br;
+ }
+ else if (data.g >= 0.0)
+ {
+ if (old.r >= 0.0 && old.r == data.b)
+ return old;
+ else
+ return vec2(old.r, old.g * data.g);
+ }
+ else
+ {
+ return old;
+ }
}
void main()
{
- uint lod = gl_GlobalInvocationID.z;
- float coordScale = lodScales.data[lod];
- vec2 centerOffset = imageSize(regionTexture).xy / 2.0;
- ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);
- vec2 coord = (gl_GlobalInvocationID.xy - centerOffset) * coordScale + Offset;
-
- vec2 result = vec2(0.0);
- for (int i = 0; i < TotalCloudRegions; i++)
- {
- vec3 data = circle(cloudRegions.data[i], coord);
- result = composite(result, data);
- }
-
- if (TotalCloudTornadoes > 0)
- {
- float tornadoType = -1.0;
- if (projectatmosphere_insideTornado(coord, tornadoType))
- {
- if (result.x < 0.0 || result.x == tornadoType)
- result = vec2(tornadoType, 1.0);
- }
- }
-
- if (TotalCloudHurricanes > 0)
- {
- for (int i = 0; i < TotalCloudHurricanes; i++)
- {
- result = projectatmosphere_compositeHurricane(result, projectatmosphere_sampleHurricane(cloudHurricanes.data[i], coord));
- }
- }
-
- imageStore(regionTexture, ivec3(texelCoord, lod), vec4(result, 0.0, 0.0));
+ uint lod = gl_GlobalInvocationID.z;
+ float coordScale = lodScales.data[lod];
+ vec2 centerOffset = imageSize(regionTexture).xy / 2.0;
+ ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);
+ vec2 coord = (gl_GlobalInvocationID.xy - centerOffset) * coordScale + Offset;
+
+ vec2 result = vec2(0.0);
+ for (int i = 0; i < TotalCloudRegions; i++)
+ {
+ vec3 data = circle(cloudRegions.data[i], coord);
+ result = composite(result, data);
+ }
+
+ imageStore(regionTexture, ivec3(texelCoord, lod), vec4(result, 0.0, 0.0));
}
diff --git a/src/main/resources/assets/projectatmosphere/shaders/compute/cube_mesh.comp b/src/main/resources/assets/projectatmosphere/shaders/compute/cube_mesh.comp
index 337bac98..66361b16 100644
--- a/src/main/resources/assets/projectatmosphere/shaders/compute/cube_mesh.comp
+++ b/src/main/resources/assets/projectatmosphere/shaders/compute/cube_mesh.comp
@@ -15,10 +15,6 @@ layout(local_size_x = ${LOCAL_SIZE_X}, local_size_y = ${LOCAL_SIZE_Y}, local_siz
#moj_import
-bool projectatmosphere_insideTornado(vec3 pos, float typeIndex);
-int projectatmosphere_findHurricane(vec2 pos, float typeIndex);
-float projectatmosphere_getStormHeightSample(float y, int hurricaneIndex);
-
struct LayerGroup {
int StartIndex;
int EndIndex;
@@ -140,36 +136,10 @@ layout(std430) readonly buffer NoiseLayers {
layers;
layout(std430) readonly buffer LayerGroupings {
- LayerGroup data[];
+ LayerGroup data[];
}
layerGroupings;
-struct CloudTornado {
- float typeIndex;
- vec2 center;
- float radius;
- float bottom;
- float height;
- vec2 padding;
-};
-
-layout(std430) readonly buffer CloudTornadoes {
- CloudTornado data[];
-}
-cloudTornadoes;
-
-struct CloudHurricane {
- vec4 shape0;
- vec4 shape1;
- vec4 shape2;
- vec4 shape3;
-};
-
-layout(std430) readonly buffer CloudHurricanes {
- CloudHurricane data[];
-}
-cloudHurricanes;
-
#if TYPE == 0
uniform sampler2DArray RegionsSampler;
uniform int RegionsTexSize;
@@ -185,8 +155,6 @@ uniform vec3 Origin;
uniform bool TestFacesFacingAway;
uniform int DoNotOccludeSide = -1;
uniform int ChunkIndex;
-uniform int TotalCloudTornadoes;
-uniform int TotalCloudHurricanes;
#if FIXED_SECTION_SIZE == 1
//Offset in number of mesh elements
@@ -266,35 +234,24 @@ bool isPosValid(float x, float y, float z, LayerGroup group, float fade)
return getNoiseForLayerGroup(group, x, y, z, gradient) + fade > 0.0;
}
-bool isPosValid(float x, float y, float z, LayerGroup group, float fade, int hurricaneIndex)
-{
- vec3 gradient = vec3(0.0);
- float sampleY = projectatmosphere_getStormHeightSample(y, hurricaneIndex);
- return getNoiseForLayerGroup(group, x, sampleY, z, gradient) + fade > 0.0;
-}
-
bool isPosValid(float x, float y, float z, int nx, int nz)
{
#if TYPE == 0
- vec2 texelCoord = gl_GlobalInvocationID.xz + RegionSampleOffset + vec2(nx, nz);
+ vec2 texelCoord = gl_GlobalInvocationID.xz + RegionSampleOffset + vec2(nx, nz);
vec4 info = texture(RegionsSampler, vec3(texelCoord / RegionsTexSize, float(LodLevel)));
uint regionId = uint(info.r);
LayerGroup group = layerGroupings.data[regionId];
float fade = -5.0 * pow(1.0 - info.g, 10.0);
- if (TotalCloudTornadoes > 0 && projectatmosphere_insideTornado(vec3(x, y, z), info.r))
- return false;
- int hurricaneIndex = TotalCloudHurricanes > 0 ? projectatmosphere_findHurricane(vec2(x, z), info.r) : -1;
#if FADE_NEAR_ORIGIN == 1
float len = distance(vec2(x, z), Origin.xz);
- fade = min(fade, -5.0 * (1.0 - min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0)));
+ fade = min(fade, -5.0 * (1.0 - min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0)));
#endif
#elif TYPE == 1
LayerGroup group = layerGroupings.data[0];
float len = distance(vec2(x, z), Origin.xz);
float fade = -5.0 * min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0);
- int hurricaneIndex = -1;
#endif
- return isPosValid(x, y, z, group, fade, hurricaneIndex);
+ return isPosValid(x, y, z, group, fade);
}
bool shouldNotOcclude(int index)
@@ -348,16 +305,16 @@ void createFace(vec3 center, float cubeRadius, int index, float brightness)
#endif
}
-void createCube(float x, float y, float z, float cubeRadius, float brightness, float fade, LayerGroup group, int hurricaneIndex)
+void createCube(float x, float y, float z, float cubeRadius, float brightness, float fade, LayerGroup group)
{
vec3 pos = vec3(x, y, z);
vec3 norm = normalize(pos - Origin);
vec3 center = pos + cubeRadius;
//-Y
- if ((TestFacesFacingAway || dot(norm, vec3(0.0, -1.0, 0.0)) <= 0.0) && (!isPosValid(x, y - Scale, z, group, fade, hurricaneIndex) || shouldNotOcclude(2)))
+ if ((TestFacesFacingAway || dot(norm, vec3(0.0, -1.0, 0.0)) <= 0.0) && (!isPosValid(x, y - Scale, z, group, fade) || shouldNotOcclude(2)))
createFace(center, cubeRadius, 2, brightness);
//+Y
- if ((TestFacesFacingAway || dot(norm, vec3(0.0, 1.0, 0.0)) <= 0.0) && (!isPosValid(x, y + Scale, z, group, fade, hurricaneIndex) || shouldNotOcclude(3)))
+ if ((TestFacesFacingAway || dot(norm, vec3(0.0, 1.0, 0.0)) <= 0.0) && (!isPosValid(x, y + Scale, z, group, fade) || shouldNotOcclude(3)))
createFace(center, cubeRadius, 3, brightness);
//-X
if ((TestFacesFacingAway || dot(norm, vec3(-1.0, 0.0, 0.0)) <= 0.0) && (!isPosValid(x - Scale, y, z, -1, 0) || shouldNotOcclude(0)))
@@ -411,12 +368,11 @@ void main()
float z = id.z * Scale + RenderOffset.z;
#if TYPE == 0
- vec2 texelCoord = gl_GlobalInvocationID.xz + RegionSampleOffset;
+ vec2 texelCoord = gl_GlobalInvocationID.xz + RegionSampleOffset;
vec4 info = texture(RegionsSampler, vec3(texelCoord / RegionsTexSize, float(LodLevel)));
uint regionId = uint(info.r);
LayerGroup group = layerGroupings.data[regionId];
float fade = -5.0 * pow(1.0 - info.g, 10.0);
- int hurricaneIndex = TotalCloudHurricanes > 0 ? projectatmosphere_findHurricane(vec2(x, z), info.r) : -1;
#if FADE_NEAR_ORIGIN == 1
float len = distance(vec2(x, z), Origin.xz);
fade = min(fade, -5.0 * (1.0 - min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0)));
@@ -425,13 +381,11 @@ void main()
LayerGroup group = layerGroupings.data[0];
float len = distance(vec2(x, z), Origin.xz);
float fade = -5.0 * min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0);
- int hurricaneIndex = -1;
#endif
vec3 gradient = vec3(0.0);
- float sampleY = projectatmosphere_getStormHeightSample(y, hurricaneIndex);
- float noise = getNoiseForLayerGroup(group, x, sampleY, z, gradient) + fade;
+ float noise = getNoiseForLayerGroup(group, x, y, z, gradient) + fade;
float storminess = clamp(group.Storminess + fade * 0.1, 0.0, 1.0);
- float brightness = clamp(1.0 - storminess * (1.0 - clamp((sampleY - group.StormStart) / group.StormFadeDistance, 0.0, 1.0)), 0.0, 1.0);
+ float brightness = clamp(1.0 - storminess * (1.0 - clamp((y - group.StormStart) / group.StormFadeDistance, 0.0, 1.0)), 0.0, 1.0);
#if STYLE == 1
gradient = normalize(gradient);
float strength = dot(gradient, SHADE_DIRECTION) * 0.5 + 0.5;
@@ -439,7 +393,7 @@ void main()
#endif
if (noise > 0.0)
{
- createCube(x, y, z, Scale / 2.0, brightness, fade, group, hurricaneIndex);
+ createCube(x, y, z, Scale / 2.0, brightness, fade, group);
}
#if TRANSPARENCY == 1
else if (group.TransparencyFade > 0.01 && noise > -group.TransparencyFade && noise < 0.0)
@@ -453,52 +407,3 @@ void main()
}
#endif
}
-
-bool projectatmosphere_insideTornado(vec3 pos, float typeIndex)
-{
- for (int i = 0; i < TotalCloudTornadoes; i++)
- {
- CloudTornado tornado = cloudTornadoes.data[i];
- if (abs(tornado.typeIndex - typeIndex) > 0.5)
- continue;
- if (pos.y < tornado.bottom || pos.y > tornado.bottom + tornado.height)
- continue;
- if (distance(pos.xz, tornado.center) <= tornado.radius)
- return true;
- }
- return false;
-}
-
-int projectatmosphere_findHurricane(vec2 pos, float typeIndex)
-{
- int bestIndex = -1;
- float bestDistSq = 3.402823466e+38;
- for (int i = 0; i < TotalCloudHurricanes; i++)
- {
- CloudHurricane hurricane = cloudHurricanes.data[i];
- if (abs(hurricane.shape0.x - typeIndex) > 0.5)
- continue;
- vec2 delta = pos - hurricane.shape0.yz;
- float radius = hurricane.shape1.y + hurricane.shape1.w;
- float distSq = dot(delta, delta);
- if (distSq > radius * radius)
- continue;
- if (distSq < bestDistSq)
- {
- bestDistSq = distSq;
- bestIndex = i;
- }
- }
- return bestIndex;
-}
-
-float projectatmosphere_getStormHeightSample(float y, int hurricaneIndex)
-{
- if (hurricaneIndex < 0)
- return y;
- return y - cloudHurricanes.data[hurricaneIndex].shape0.w;
-}
-
-
-
-
diff --git a/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.fsh b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.fsh
new file mode 100644
index 00000000..f938b98f
--- /dev/null
+++ b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.fsh
@@ -0,0 +1,700 @@
+#version 150
+
+uniform sampler2D TornadoSampler;
+uniform sampler2D NoiseSampler;
+uniform sampler2D FlowSampler;
+uniform sampler2D DepthSampler;
+
+uniform mat4 ModelViewMat;
+uniform mat4 ProjMat;
+uniform mat4 InverseProjMat;
+uniform mat4 InverseModelViewMat;
+uniform vec3 CameraPos;
+uniform vec4 CloudColor;
+uniform float FogStart;
+uniform float FogEnd;
+uniform vec4 FogColor;
+uniform float AnimationTime;
+uniform float MaxDistance;
+uniform vec2 OutSize;
+uniform vec3 VolumeMin;
+uniform vec3 VolumeMax;
+uniform float CloudScale;
+uniform float RenderQuality;
+uniform int StormCount;
+uniform int DebugMode;
+uniform int DebugSelectedStorm;
+uniform int DebugFreeze;
+uniform float StormPositions[24];
+uniform float StormHeights[8];
+uniform float StormWidths[8];
+uniform float StormSizes[8];
+uniform float StormSpins[8];
+uniform float StormIntensities[8];
+uniform float StormShapes[8];
+uniform float StormProgress[8];
+
+in vec2 texCoord;
+in vec3 fragPos;
+out vec4 fragColor;
+
+const float PI = 3.1415926535897932384626433832795;
+const float TAU = 6.2831853071795864769252867665590;
+const int MAX_STORMS_COUNT = 8;
+const int DEBUG_OFF = 0;
+const int DEBUG_BOX = 1;
+const int DEBUG_HIT = 2;
+const int DEBUG_FILL = 3;
+const int DEBUG_FUNNEL = 4;
+const int DEBUG_HEIGHT = 5;
+const int DEBUG_RADIAL = 6;
+const int DEBUG_RADIUS = 7;
+const int DEBUG_DENSITY = 8;
+const int DEBUG_ALPHA = 9;
+const int DEBUG_WALLCLOUD = 10;
+const int DEBUG_CONNECTION = 11;
+const int DEBUG_FULL = 12;
+const float TORNADO_ROTATION_SPEED_MULTIPLIER = 3.0;
+const float TORNADO_TOP_DARKEN_FACTOR = 0.45;
+
+struct StormSample {
+ float cloud;
+ float dust;
+ float upper;
+ float material;
+};
+
+struct DebugMasks {
+ float heightMask;
+ float radialMask;
+ float radiusMask;
+ float density;
+ float alpha;
+ float wallcloud;
+ float connection;
+};
+
+float saturate(float value) {
+ return clamp(value, 0.0, 1.0);
+}
+
+float hash1(float p) {
+ p = fract(p * 0.1031);
+ p *= p + 33.33;
+ p *= p + p;
+ return fract(p);
+}
+
+float onoise(vec3 pos) {
+ vec3 x = pos * 2.0;
+ vec3 p = floor(x);
+ vec3 f = fract(x);
+ f = f * f * (3.0 - 2.0 * f);
+ float n = p.x + p.y * 57.0 + 113.0 * p.z;
+ return mix(
+ mix(
+ mix(hash1(n + 0.0), hash1(n + 1.0), f.x),
+ mix(hash1(n + 57.0), hash1(n + 58.0), f.x),
+ f.y
+ ),
+ mix(
+ mix(hash1(n + 113.0), hash1(n + 114.0), f.x),
+ mix(hash1(n + 170.0), hash1(n + 171.0), f.x),
+ f.y
+ ),
+ f.z
+ );
+}
+
+float noise3(vec3 p) {
+ vec3 i = floor(p);
+ vec3 f = fract(p);
+ f = f * f * (3.0 - 2.0 * f);
+
+ float n000 = hash1(dot(i + vec3(0.0, 0.0, 0.0), vec3(1.0, 57.0, 113.0)));
+ float n100 = hash1(dot(i + vec3(1.0, 0.0, 0.0), vec3(1.0, 57.0, 113.0)));
+ float n010 = hash1(dot(i + vec3(0.0, 1.0, 0.0), vec3(1.0, 57.0, 113.0)));
+ float n110 = hash1(dot(i + vec3(1.0, 1.0, 0.0), vec3(1.0, 57.0, 113.0)));
+ float n001 = hash1(dot(i + vec3(0.0, 0.0, 1.0), vec3(1.0, 57.0, 113.0)));
+ float n101 = hash1(dot(i + vec3(1.0, 0.0, 1.0), vec3(1.0, 57.0, 113.0)));
+ float n011 = hash1(dot(i + vec3(0.0, 1.0, 1.0), vec3(1.0, 57.0, 113.0)));
+ float n111 = hash1(dot(i + vec3(1.0, 1.0, 1.0), vec3(1.0, 57.0, 113.0)));
+
+ float x00 = mix(n000, n100, f.x);
+ float x10 = mix(n010, n110, f.x);
+ float x01 = mix(n001, n101, f.x);
+ float x11 = mix(n011, n111, f.x);
+ float y0 = mix(x00, x10, f.y);
+ float y1 = mix(x01, x11, f.y);
+ return mix(y0, y1, f.z) * 2.0 - 1.0;
+}
+
+float fbm(vec3 x, int octaves, float lacunarity, float gain, float amplitude) {
+ float y = 0.0;
+ for (int i = 0; i < octaves; i++) {
+ y += amplitude * noise3(x);
+ x *= lacunarity;
+ amplitude *= gain;
+ }
+ return y;
+}
+
+mat2 spin(float angle) {
+ return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
+}
+
+float cloudToWorld(float value) {
+ return value * CloudScale;
+}
+
+vec3 cloudToWorld(vec3 value) {
+ return value * CloudScale;
+}
+
+vec3 reconstructPosition(vec2 uv, float depth) {
+ vec4 ndc = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
+ vec4 clip = InverseProjMat * ndc;
+ clip /= clip.w;
+ vec4 result = InverseModelViewMat * clip;
+ return result.xyz / result.w;
+}
+
+float cloudSpaceToDepth(vec3 pos) {
+ vec4 clip = ProjMat * ModelViewMat * vec4(pos, 1.0);
+ float ndcZ = clip.z / clip.w;
+ return ndcZ * 0.5 + 0.5;
+}
+
+bool intersectAabb(vec3 ro, vec3 rd, vec3 bmin, vec3 bmax, out float tNear, out float tFar) {
+ vec3 inv = 1.0 / rd;
+ vec3 t0 = (bmin - ro) * inv;
+ vec3 t1 = (bmax - ro) * inv;
+ vec3 tsmaller = min(t0, t1);
+ vec3 tbigger = max(t0, t1);
+ tNear = max(max(tsmaller.x, tsmaller.y), tsmaller.z);
+ tFar = min(min(tbigger.x, tbigger.y), tbigger.z);
+ return tFar > max(tNear, 0.0);
+}
+
+vec3 getStormPos(int index) {
+ return vec3(StormPositions[index * 3], StormPositions[index * 3 + 1], StormPositions[index * 3 + 2]);
+}
+
+float computeStormDetailQuality(int index) {
+ vec3 cameraPosWorld = cloudToWorld(CameraPos);
+ vec3 stormPosWorld = cloudToWorld(getStormPos(index));
+ float horizontalDistanceWorld = distance(cameraPosWorld.xz, stormPosWorld.xz);
+ float distanceQuality = 1.0 - smoothstep(96.0, 320.0, horizontalDistanceWorld);
+ float screenProxyQuality = saturate(cloudToWorld(StormWidths[index]) / 42.0);
+ float retainedQuality = max(distanceQuality, screenProxyQuality * 0.45);
+ return clamp(RenderQuality * mix(0.42, 1.0, retainedQuality), 0.25, 1.0);
+}
+
+float sampleFunnelRadiusWorld(float widthWorld, float stormSizeWorld, float tornadoShape, float torPerc, float percFnlHeight) {
+ float torShape = mix(tornadoShape, 20.0, saturate(widthWorld / 62.5));
+ float widWorld = (widthWorld / 2.5)
+ + ((widthWorld / 2.5) * percFnlHeight * torPerc)
+ + ((stormSizeWorld / mix(torShape + 2.0, torShape, torPerc)) * pow(percFnlHeight, 4.0));
+ return mix(widWorld, 0.0, (1.0 - percFnlHeight) * (1.0 - torPerc));
+}
+
+float sampleMaterialField(vec3 localTorPosWorld, float percFnlHeight, float widPerc, float widWorld,
+ float spinPhaseA, float spinPhaseB, float animTime, float detailQuality, bool freezeDebug) {
+ if (freezeDebug) {
+ return 1.0;
+ }
+
+ if (detailQuality < 0.45) {
+ float coarse = noise3(vec3(localTorPosWorld.xz * 0.055, percFnlHeight * 2.2 + animTime * 0.018)) * 0.5 + 0.5;
+ float band = noise3(vec3(localTorPosWorld.xz * 0.028 + vec2(animTime * 0.038, -animTime * 0.032), percFnlHeight * 1.35)) * 0.5 + 0.5;
+ float turbulence = saturate(coarse * 0.68 + band * 0.32);
+ return mix(0.90, 1.12, turbulence) * mix(0.90, 1.08, widPerc);
+ }
+
+ if (detailQuality < 0.72) {
+ vec2 swirl = spin(spinPhaseA) * localTorPosWorld.xz;
+ vec2 flow = texture(
+ FlowSampler,
+ fract(swirl * 0.024 + vec2(animTime * 0.014, -animTime * 0.012))
+ ).rg * 2.0 - 1.0;
+ vec2 uv = fract(swirl * 0.064 + flow * 0.052 + vec2(percFnlHeight * 0.24, animTime * 0.034));
+ vec4 tex = texture(TornadoSampler, uv);
+ float lum = max(tex.a, dot(tex.rgb, vec3(0.299, 0.587, 0.114)));
+ float noise = texture(NoiseSampler, fract(swirl * 0.030 + vec2(0.17, 0.63))).r;
+ float turbulence = saturate(lum * 0.74 + noise * 0.26);
+ return mix(0.86, 1.14, turbulence) * mix(0.90, 1.12, widPerc);
+ }
+
+ vec2 swirlA = spin(spinPhaseA) * localTorPosWorld.xz;
+ vec2 swirlB = spin(spinPhaseB) * localTorPosWorld.xz;
+
+ vec2 flowA = texture(
+ FlowSampler,
+ fract(swirlA * 0.020 + vec2(animTime * 0.012, -animTime * 0.014))
+ ).rg * 2.0 - 1.0;
+ vec2 flowB = texture(
+ FlowSampler,
+ fract(swirlB * 0.033 + vec2(-animTime * 0.018, animTime * 0.016))
+ ).rg * 2.0 - 1.0;
+
+ vec2 uvA = fract(swirlA * 0.055 + flowA * 0.060 + vec2(percFnlHeight * 0.30, animTime * 0.030));
+ vec2 uvB = fract(swirlB * 0.095 + flowB * 0.040 + vec2(-animTime * 0.042, percFnlHeight * 0.88));
+ vec4 texA = texture(TornadoSampler, uvA);
+ vec4 texB = texture(TornadoSampler, uvB);
+ float lumA = max(texA.a, dot(texA.rgb, vec3(0.299, 0.587, 0.114)));
+ float lumB = max(texB.a, dot(texB.rgb, vec3(0.299, 0.587, 0.114)));
+
+ float noiseA = texture(NoiseSampler, fract(swirlA * 0.020 + vec2(0.17, 0.63))).r;
+ float noiseB = texture(NoiseSampler, fract(swirlB * 0.037 + vec2(0.48, 0.22))).r;
+ float planarField = mix(lumA, lumB, 0.58);
+
+ float radial = length(localTorPosWorld.xz);
+ float angular = atan(localTorPosWorld.z, localTorPosWorld.x) / TAU + 0.5;
+ vec2 cylFlow = texture(
+ FlowSampler,
+ fract(vec2(angular * 1.35 + animTime * 0.022, percFnlHeight * 1.20 - animTime * 0.016) + vec2(0.19, 0.43))
+ ).rg * 2.0 - 1.0;
+ vec2 cylUvA = fract(vec2(
+ angular * 2.60 + animTime * 0.085 + cylFlow.x * 0.08,
+ percFnlHeight * 1.55 + radial * 0.050 + cylFlow.y * 0.06
+ ));
+ vec2 cylUvB = fract(vec2(
+ angular * 4.10 - animTime * 0.132 - cylFlow.y * 0.05,
+ percFnlHeight * 2.05 - radial * 0.032 + cylFlow.x * 0.04
+ ));
+ vec4 cylTexA = texture(TornadoSampler, cylUvA);
+ vec4 cylTexB = texture(TornadoSampler, cylUvB);
+ float cylLumA = max(cylTexA.a, dot(cylTexA.rgb, vec3(0.299, 0.587, 0.114)));
+ float cylLumB = max(cylTexB.a, dot(cylTexB.rgb, vec3(0.299, 0.587, 0.114)));
+ float cylindricalField = mix(cylLumA, cylLumB, 0.52);
+
+ float lowerBlend = 1.0 - smoothstep(0.18, 0.46, percFnlHeight);
+ lowerBlend *= mix(0.55, 1.0, widPerc);
+ lowerBlend *= 1.0 - smoothstep(1.2, 3.0, widWorld);
+
+ float texField = mix(planarField, cylindricalField, lowerBlend);
+ float turbulence = saturate(texField * 0.72 + noiseA * 0.18 + noiseB * 0.10);
+ return mix(0.84, 1.18, turbulence) * mix(0.88, 1.14, widPerc);
+}
+
+DebugMasks sampleDebugMasks(int index, vec3 position) {
+ vec3 pos = getStormPos(index);
+ vec3 posWorld = cloudToWorld(pos);
+ vec3 positionWorld = cloudToWorld(position);
+ float widthWorld = max(cloudToWorld(StormWidths[index]), 0.001);
+ float stormSizeWorld = max(cloudToWorld(StormSizes[index]), widthWorld * 2.0);
+ float baseHeightWorld = cloudToWorld(pos.y + StormHeights[index]);
+ float intensity = saturate(StormIntensities[index]);
+ float torPerc = saturate(StormProgress[index]);
+ float tornadoShape = StormShapes[index];
+
+ float funnelTopWorld = max(baseHeightWorld - 13.125, posWorld.y + 3.75);
+ float heightMask = 0.0;
+ if (positionWorld.y >= posWorld.y && positionWorld.y <= funnelTopWorld) {
+ heightMask = saturate((positionWorld.y - posWorld.y) / max(funnelTopWorld - posWorld.y, 0.001));
+ }
+
+ float funnelRadiusWorld = sampleFunnelRadiusWorld(widthWorld, stormSizeWorld, tornadoShape, torPerc, heightMask);
+ float radialDistanceWorld = distance(positionWorld.xz, posWorld.xz);
+ float radialMask = 1.0 - saturate(radialDistanceWorld / max(funnelRadiusWorld, 0.001));
+ float verticalGate = step(posWorld.y, positionWorld.y) * (1.0 - step(funnelTopWorld, positionWorld.y));
+ float density = radialMask * verticalGate;
+ float alpha = saturate(density * 0.85);
+
+ float wallcloudRadiusWorld = stormSizeWorld * 0.35;
+ float wallcloudLowerWorld = 15.0 * pow(max(1.0 - saturate(radialDistanceWorld / max(wallcloudRadiusWorld, 0.001)), 0.0), 0.25) * saturate((intensity - 0.45) * 2.2);
+ float wallcloud = 0.0;
+ if (positionWorld.y <= baseHeightWorld && positionWorld.y >= baseHeightWorld - wallcloudLowerWorld) {
+ float wallPerc = 1.0 - saturate(radialDistanceWorld / max(wallcloudRadiusWorld, 0.001));
+ wallcloud = pow(max(wallPerc, 0.0), 0.55) * saturate((intensity - 0.40) * 2.6);
+ }
+
+ float connectionRadiusWorld = max(funnelRadiusWorld * mix(1.8, 2.5, intensity), stormSizeWorld * 0.28);
+ float connectionPerc = 1.0 - saturate(radialDistanceWorld / max(connectionRadiusWorld, 0.001));
+ float connection = pow(max(connectionPerc, 0.0), 0.55);
+ connection *= smoothstep(funnelTopWorld - 1.8, baseHeightWorld + 1.8, positionWorld.y);
+ connection *= saturate((intensity - 0.25) * 1.7);
+
+ DebugMasks masks;
+ masks.heightMask = heightMask * verticalGate;
+ masks.radialMask = radialMask;
+ masks.radiusMask = saturate(funnelRadiusWorld / max(stormSizeWorld, 0.001));
+ masks.density = density;
+ masks.alpha = alpha;
+ masks.wallcloud = wallcloud;
+ masks.connection = connection;
+ return masks;
+}
+
+float selectDebugMask(DebugMasks masks) {
+ if (DebugMode == DEBUG_FUNNEL) {
+ return max(masks.density, max(masks.wallcloud, masks.connection));
+ }
+ if (DebugMode == DEBUG_HEIGHT) {
+ return masks.heightMask;
+ }
+ if (DebugMode == DEBUG_RADIAL) {
+ return masks.radialMask;
+ }
+ if (DebugMode == DEBUG_RADIUS) {
+ return masks.radiusMask;
+ }
+ if (DebugMode == DEBUG_DENSITY) {
+ return masks.density;
+ }
+ if (DebugMode == DEBUG_ALPHA) {
+ return masks.alpha;
+ }
+ if (DebugMode == DEBUG_WALLCLOUD) {
+ return masks.wallcloud;
+ }
+ if (DebugMode == DEBUG_CONNECTION) {
+ return masks.connection;
+ }
+ return 0.0;
+}
+
+StormSample sampleFrozenStorm(int index, vec3 position) {
+ DebugMasks masks = sampleDebugMasks(index, position);
+ StormSample outSample;
+ outSample.cloud = max(masks.density, max(masks.wallcloud, masks.connection * 0.9));
+ outSample.dust = 0.0;
+ outSample.upper = max(masks.wallcloud, masks.connection);
+ outSample.material = saturate(masks.alpha + masks.connection * 0.25);
+ return outSample;
+}
+
+StormSample sampleStorm(int index, vec3 position) {
+ vec3 pos = getStormPos(index);
+ vec3 posWorld = cloudToWorld(pos);
+ vec3 positionWorld = cloudToWorld(position);
+ float baseHeightWorld = cloudToWorld(pos.y + StormHeights[index]);
+ float widthWorld = max(cloudToWorld(StormWidths[index]), 0.001);
+ float stormSizeWorld = max(cloudToWorld(StormSizes[index]), widthWorld * 2.0);
+ float stormSpin = StormSpins[index];
+ float intensity = saturate(StormIntensities[index]);
+ float torPerc = saturate(StormProgress[index]);
+ float tornadoShape = StormShapes[index];
+ float detailQuality = computeStormDetailQuality(index);
+ bool reducedDetail = detailQuality < 0.72;
+ bool lowDetail = detailQuality < 0.45;
+
+ float distWorld = distance(positionWorld.xz, posWorld.xz);
+ float wallcloudRadiusWorld = stormSizeWorld * 0.35;
+ float wallcloudLowerWorld = 15.0 * pow(max(1.0 - saturate(distWorld / max(wallcloudRadiusWorld, 0.001)), 0.0), 0.25) * saturate((intensity - 0.45) * 2.2);
+
+ float wallcloud = 0.0;
+ if (positionWorld.y <= baseHeightWorld && positionWorld.y >= baseHeightWorld - wallcloudLowerWorld) {
+ float wallPerc = 1.0 - saturate(distWorld / max(wallcloudRadiusWorld, 0.001));
+ wallcloud = pow(max(wallPerc, 0.0), 0.55) * saturate((intensity - 0.40) * 2.6);
+ wallcloud *= 0.7 + onoise(vec3(positionWorld.xz / 20.0, AnimationTime / 150.0)) * 0.3;
+ }
+
+ StormSample outSample;
+ outSample.cloud = wallcloud;
+ outSample.dust = 0.0;
+ outSample.upper = wallcloud;
+ outSample.material = wallcloud * 0.45;
+
+ if (!(positionWorld.y < baseHeightWorld - wallcloudLowerWorld && positionWorld.y > posWorld.y - 8.5 && distWorld < max(widthWorld * 5.0, stormSizeWorld / 2.6))) {
+ return outSample;
+ }
+
+ float fnlTopWorld = max(baseHeightWorld - 13.125, posWorld.y + 3.75);
+ float percFnlHeight = saturate((positionWorld.y - posWorld.y) / max(fnlTopWorld - posWorld.y, 0.001));
+ float percCos = (-cos(percFnlHeight * PI) + 1.0) * 0.5;
+ float torShape = mix(tornadoShape, 20.0, pow(saturate(widthWorld / 62.5), 1.75));
+ float widWorld = (widthWorld / 2.5)
+ + ((widthWorld / 2.5) * percFnlHeight * torPerc)
+ + ((stormSizeWorld / mix(torShape + 2.0, torShape, torPerc)) * pow(percFnlHeight, 4.0));
+ widWorld = mix(widWorld, 0.0, (1.0 - percFnlHeight) * (1.0 - torPerc));
+ float tornadoHeightWorld = mix(fnlTopWorld, posWorld.y - 0.25, torPerc);
+ float th = 1.0 - saturate((positionWorld.y - tornadoHeightWorld) / 3.75);
+ widWorld = mix(widWorld, 0.0, th * th * th);
+ float maxWidWorld = (widthWorld / 4.0) + ((widthWorld / 4.0) * torPerc) + ((stormSizeWorld / 8.0) * torPerc);
+
+ float ropeMod = mix(3.0, 1.0, saturate(widthWorld / 3.75));
+ ropeMod = mix(ropeMod, 1.0, saturate((intensity - 0.55) * 2.4));
+ ropeMod = mix(0.1, ropeMod, saturate(torPerc * 1.35));
+
+ float swayTime = AnimationTime / 220.0;
+ float nx = mix(
+ onoise(vec3(posWorld.xz / 62.5, swayTime)),
+ noise3(vec3(posWorld.xz / 35.0, swayTime * 0.6)),
+ 0.35
+ ) * 5.0 * ropeMod;
+ float nz = mix(
+ onoise(vec3(swayTime, posWorld.zx / 62.5)),
+ noise3(vec3(swayTime * 0.6, posWorld.zx / 35.0)),
+ 0.35
+ ) * 5.0 * ropeMod;
+ vec3 attachmentPointWorld = vec3(nx, 0.0, nz);
+
+ float xAdd = mix(
+ onoise(vec3(posWorld.xz / 31.25, swayTime + ((positionWorld.y * ropeMod) / 6.25))),
+ noise3(vec3(posWorld.xz / 18.0, (swayTime * 0.8) + ((positionWorld.y * ropeMod) / 9.5))),
+ 0.30
+ ) * 2.5 * ropeMod;
+ float zAdd = mix(
+ onoise(vec3(swayTime + ((positionWorld.y * ropeMod) / 6.25), posWorld.zx / 31.25)),
+ noise3(vec3((swayTime * 0.8) + ((positionWorld.y * ropeMod) / 9.5), posWorld.zx / 18.0)),
+ 0.30
+ ) * 2.5 * ropeMod;
+ float a = pow(percFnlHeight, 0.75);
+ xAdd *= a;
+ zAdd *= a;
+
+ vec3 torPosWorld = posWorld + mix(vec3(0.0), vec3(attachmentPointWorld.x, 0.0, attachmentPointWorld.z), percCos) + vec3(xAdd, 0.0, zAdd);
+ float torDistWorld = distance(torPosWorld.xz, positionWorld.xz);
+ vec3 localTorPosWorld = positionWorld - torPosWorld;
+
+ float influenceRadiusWorld = max(
+ max(widWorld * mix(1.8, 2.5, intensity), stormSizeWorld * 0.28),
+ max((widthWorld / 1.5) + 6.25, widthWorld * 1.8)
+ );
+ if (torDistWorld > influenceRadiusWorld) {
+ return outSample;
+ }
+
+ float widPerc = 1.0 - saturate(torDistWorld / max(widWorld, 0.001));
+ float widMaxPerc = saturate(widWorld / max(maxWidWorld, 0.001));
+ float rotation = -stormSpin * 3.0 * TORNADO_ROTATION_SPEED_MULTIPLIER;
+ float rotation2 = (-stormSpin / 1.5) * TORNADO_ROTATION_SPEED_MULTIPLIER;
+
+ mat2 torSpin = spin(rotation + (torDistWorld / 6.25));
+ mat2 torSpin2 = spin(rotation2 + (torDistWorld / 18.75));
+ mat2 torSpin3 = spin(rotation2 + (torDistWorld / 7.5));
+ vec3 torSpinPos = vec3(torSpin * localTorPosWorld.xz, positionWorld.y - (AnimationTime / 2.0));
+ vec3 torSpinPos2 = vec3(torSpin2 * localTorPosWorld.xz, positionWorld.y - (AnimationTime / 2.0));
+ vec3 torSpinPos3 = vec3(torSpin3 * localTorPosWorld.xz, positionWorld.y - (AnimationTime / 2.0));
+
+ int primaryOctaves = lowDetail ? 1 : (reducedDetail ? 2 : 3);
+ int secondaryOctaves = lowDetail ? 1 : 2;
+ int contactOctaves = lowDetail ? 1 : (reducedDetail ? 2 : 3);
+ float nComp1 = fbm(torSpinPos / 2.5, primaryOctaves, 2.0, 0.5, 1.0);
+ float nComp2 = fbm(torSpinPos2 / 5.0, primaryOctaves, 2.0, 0.5, 1.0);
+ float torNoise1 = mix(nComp1, nComp2, sqrt(widMaxPerc));
+ float torNoise2 = lowDetail
+ ? noise3((torSpinPos + vec3(9.2, -5.7, 3.1)) / 1.6)
+ : fbm((torSpinPos + vec3(9.2, -5.7, 3.1)) / 1.6, secondaryOctaves, 2.0, 0.55, 1.0);
+
+ widWorld *= mix(0.8 + (torNoise1 * 0.2), 0.9, saturate(widthWorld / 125.0) * 0.9);
+ widWorld *= 1.0 + torNoise2 * 0.035;
+ widPerc = 1.0 - saturate(torDistWorld / max(widWorld, 0.001));
+
+ float materialField = sampleMaterialField(
+ localTorPosWorld,
+ percFnlHeight,
+ widPerc,
+ widWorld,
+ rotation + (torDistWorld / 6.25),
+ rotation2 + (torDistWorld / 18.75),
+ AnimationTime,
+ detailQuality,
+ false
+ );
+ float innerDensity = pow(max(widPerc, 0.0), mix(1.15, 1.55, 1.0 - intensity)) * 4.0;
+ float shearBand = saturate(widPerc * (1.0 - widPerc) * 4.0);
+ float shellDensity = shearBand * (0.92 + materialField * 0.34) * mix(0.95, 1.35, intensity);
+ float coreFill = pow(max(widPerc, 0.0), mix(2.8, 1.45, intensity)) * (0.22 + materialField * 0.20);
+ coreFill *= smoothstep(posWorld.y - 0.8, fnlTopWorld, positionWorld.y);
+ float innerVeil = smoothstep(0.18, 0.74, widPerc) * (1.0 - smoothstep(0.78, 0.98, widPerc));
+ innerVeil *= 0.18 + intensity * 0.16;
+ float turbulence = 0.82 + (torNoise1 * 0.14) + (torNoise2 * 0.08);
+ float tornado = innerDensity * saturate((positionWorld.y - tornadoHeightWorld) / 2.5) * turbulence;
+ tornado += shearBand * 0.55 * (0.85 + materialField * 0.25);
+ tornado += shellDensity * 0.78;
+ tornado += coreFill * 0.65;
+ tornado += innerVeil * (0.80 + materialField * 0.20);
+ tornado *= materialField;
+ tornado *= mix(0.78, 1.06, intensity);
+
+ float dcNoise1 = lowDetail
+ ? noise3(torSpinPos3 / 2.5)
+ : fbm(torSpinPos3 / 2.5, contactOctaves, 2.0, 0.5, 1.0);
+ float baseContactRadiusWorld = max(widthWorld * 0.24, 0.72) + intensity * 0.42;
+ float baseContactPerc = 1.0 - saturate(torDistWorld / max(baseContactRadiusWorld, 0.001));
+ float touchdown = pow(max(baseContactPerc, 0.0), 0.48);
+ touchdown *= saturate((positionWorld.y - (posWorld.y - 2.2)) / 2.6);
+ touchdown *= 1.0 - saturate((positionWorld.y - (posWorld.y + 1.8)) / 3.4);
+ touchdown *= 0.72 + dcNoise1 * 0.12;
+ tornado = max(tornado, touchdown * 1.05);
+
+ float dcPerc = saturate((intensity - 0.35) * 1.9);
+ float h = 5.0 + (dcNoise1 * 1.875);
+ float dcTopWorld = posWorld.y + (max(dcPerc, 0.35) * h);
+ float percDCHeight = saturate((positionWorld.y - (posWorld.y - 1.25)) / max(dcTopWorld - posWorld.y, 0.001));
+
+ float dustWidWorld = ((widthWorld / 1.5) + ((widthWorld / 1.5) * percFnlHeight * torPerc) + 3.125) + (3.125 * pow(percDCHeight, 1.5) * pow(dcPerc, 0.75));
+ dustWidWorld *= mix(0.6 + (dcNoise1 * 0.5), 0.85, saturate(widthWorld / 62.5) * 0.9);
+ float dustWidPerc = 1.0 - saturate(torDistWorld / max(dustWidWorld, 0.001));
+ dustWidPerc = pow(max(dustWidPerc, 0.0), 0.25);
+ float edge = saturate(torDistWorld / max(dustWidWorld * 0.9, 0.001));
+ dustWidPerc *= edge * edge * edge;
+ float dust = 0.0;
+ if (!lowDetail) {
+ dust = pow(max(dustWidPerc, 0.0), 1.5) * 0.15;
+ dust *= saturate((dcTopWorld - positionWorld.y) / 2.5);
+ dust *= saturate((positionWorld.y - (posWorld.y - 2.5)) / 2.5);
+ dust *= 0.8 + (dcNoise1 * 0.2);
+ dust *= dcPerc;
+ dust *= 1.0 - saturate((widthWorld - 6.25) / 25.0);
+ }
+
+ float connectionRadiusWorld = max(widWorld * mix(1.8, 2.5, intensity), stormSizeWorld * 0.28);
+ float connectionPerc = 1.0 - saturate(torDistWorld / max(connectionRadiusWorld, 0.001));
+ float connection = pow(max(connectionPerc, 0.0), 0.55);
+ connection *= smoothstep(fnlTopWorld - 1.8, baseHeightWorld + 1.8, positionWorld.y);
+ connection *= saturate((intensity - 0.25) * 1.7);
+ if (!reducedDetail) {
+ connection *= 0.82 + onoise(vec3((positionWorld.xz + posWorld.xz) / 20.0, AnimationTime / 140.0)) * 0.18;
+ }
+
+ outSample.cloud = max(outSample.cloud, max(tornado, dust));
+ outSample.cloud = max(outSample.cloud, connection * 0.9);
+ outSample.dust = max(outSample.dust, dust);
+ outSample.upper = max(outSample.upper, wallcloud + tornado * smoothstep(0.68, 1.0, percFnlHeight) * 0.30);
+ outSample.upper = max(outSample.upper, connection);
+ outSample.material = max(outSample.material, materialField * saturate(tornado + connection * 0.35));
+ return outSample;
+}
+
+void main() {
+ if (DebugMode == DEBUG_BOX) {
+ fragColor = vec4(0.95, 0.28, 0.08, 1.0);
+ return;
+ }
+
+ vec3 ro = CameraPos;
+ vec3 rd = normalize(fragPos - ro);
+ float tNear;
+ float tFar;
+ if (!intersectAabb(ro, rd, VolumeMin, VolumeMax, tNear, tFar)) {
+ if (DebugMode == DEBUG_HIT) {
+ fragColor = vec4(1.0, 0.0, 1.0, 1.0);
+ return;
+ }
+ discard;
+ }
+ tNear = max(tNear, 0.0);
+
+ if (DebugMode == DEBUG_HIT) {
+ fragColor = vec4(0.10, 0.95, 0.15, 1.0);
+ return;
+ }
+
+ if (DebugMode == DEBUG_FILL) {
+ float intervalFill = max(tFar - tNear, 0.0);
+ float alphaFill = saturate(1.0 - exp(-intervalFill * 0.55));
+ fragColor = vec4(vec3(0.86), max(alphaFill, 0.65));
+ return;
+ }
+
+ vec2 screenUv = gl_FragCoord.xy / OutSize;
+ float sceneDepth = texture(DepthSampler, screenUv).r;
+ bool useSceneDepthStop = DebugMode == DEBUG_OFF;
+ float maxRay = min(tFar, MaxDistance);
+ if (useSceneDepthStop && sceneDepth < 1.0) {
+ vec3 rayEnd = reconstructPosition(screenUv, sceneDepth);
+ maxRay = min(maxRay, length(rayEnd - ro));
+ }
+ if (maxRay <= tNear + 0.001) {
+ discard;
+ }
+
+ bool debugActive = DebugMode != DEBUG_OFF;
+ bool debugMaskMode = debugActive
+ && DebugMode != DEBUG_BOX
+ && DebugMode != DEBUG_HIT
+ && DebugMode != DEBUG_FILL
+ && DebugMode != DEBUG_FULL;
+ float detailQuality = debugActive ? 1.0 : computeStormDetailQuality(0);
+
+ vec3 accum = vec3(0.0);
+ float transmittance = 1.0;
+ float nearestT = tNear;
+ float firstHitDepth = 1.0;
+ float debugValue = 0.0;
+ bool wroteDepth = false;
+
+ float interval = maxRay - tNear;
+ float stepSpacing = mix(1.40, 0.60, detailQuality);
+ float minSteps = mix(8.0, 14.0, detailQuality);
+ float maxSteps = mix(22.0, 40.0, detailQuality);
+ int steps = int(clamp(interval / stepSpacing, minSteps, maxSteps));
+ float stepSize = interval / float(max(steps, 1));
+ float jitter = hash1(screenUv.x * OutSize.x + screenUv.y * OutSize.y + 17.13);
+ float t = tNear + stepSize * (0.20 + jitter * 0.80);
+
+ for (int step = 0; step < 40; step++) {
+ if (step >= steps) {
+ break;
+ }
+ if (!debugMaskMode && transmittance < mix(0.08, 0.03, detailQuality)) {
+ break;
+ }
+
+ vec3 samplePos = ro + rd * t;
+ if (debugMaskMode) {
+ DebugMasks masks = sampleDebugMasks(0, samplePos);
+ float value = selectDebugMask(masks);
+ if (value > 0.0005) {
+ debugValue = max(debugValue, value);
+ if (!wroteDepth) {
+ firstHitDepth = clamp(cloudSpaceToDepth(samplePos), 0.0, 1.0);
+ wroteDepth = true;
+ }
+ }
+ } else {
+ StormSample storm = debugActive && DebugFreeze != 0
+ ? sampleFrozenStorm(0, samplePos)
+ : sampleStorm(0, samplePos);
+ float sigma = max(storm.cloud, 0.0) * 0.195;
+ if (sigma > 0.0005) {
+ if (!wroteDepth) {
+ firstHitDepth = clamp(cloudSpaceToDepth(samplePos), 0.0, 1.0);
+ wroteDepth = true;
+ }
+ float nearField = 1.0 - saturate(t / 12.0);
+ float alpha = 1.0 - exp(-sigma * stepSize * 8.4);
+ alpha = saturate(alpha * (1.10 + nearField * 0.32));
+ float bodyDark = mix(0.08, 0.25, saturate(storm.material));
+ vec3 cloudBase = CloudColor.rgb * bodyDark;
+ float upperStrength = saturate(storm.upper);
+ vec3 upperCol = mix(cloudBase, CloudColor.rgb * 0.64, upperStrength * 0.62);
+ upperCol *= mix(1.0, 1.0 - TORNADO_TOP_DARKEN_FACTOR, upperStrength);
+ vec3 dustCol = vec3(0.20, 0.125, 0.071);
+ float dustTint = saturate(pow(storm.dust, 0.55)) * (1.0 - saturate(storm.material * 0.45));
+ vec3 localColor = mix(upperCol, dustCol, dustTint);
+ accum += localColor * alpha * transmittance;
+ transmittance *= (1.0 - alpha);
+ }
+ }
+
+ t += stepSize;
+ }
+
+ if (debugMaskMode) {
+ if (debugValue < 0.01) {
+ discard;
+ }
+ if (wroteDepth) {
+ gl_FragDepth = firstHitDepth;
+ }
+ fragColor = vec4(vec3(debugValue), saturate(debugValue));
+ return;
+ }
+
+ float alpha = 1.0 - transmittance;
+ if (alpha < 0.01) {
+ discard;
+ }
+
+ vec3 color = accum / max(alpha, 0.0001);
+ float fogFactor = smoothstep(FogStart, FogEnd, nearestT);
+ color = mix(color, FogColor.rgb, fogFactor * 0.45);
+ if (wroteDepth) {
+ gl_FragDepth = firstHitDepth;
+ }
+ fragColor = vec4(color, saturate(alpha));
+}
diff --git a/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.json b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.json
new file mode 100644
index 00000000..c6bd647d
--- /dev/null
+++ b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.json
@@ -0,0 +1,49 @@
+{
+ "blend": {
+ "func": "add",
+ "srcrgb": "srcalpha",
+ "dstrgb": "1-srcalpha"
+ },
+ "vertex": "projectatmosphere:tornado_volume_box",
+ "fragment": "projectatmosphere:tornado_round",
+ "attributes": [
+ "Position",
+ "UV0"
+ ],
+ "samplers": [
+ { "name": "TornadoSampler" },
+ { "name": "NoiseSampler" },
+ { "name": "FlowSampler" },
+ { "name": "DepthSampler" }
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "InverseProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "InverseModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "VolumeMin", "type": "float", "count": 3, "values": [ 0.0, 0.0, 0.0 ] },
+ { "name": "VolumeMax", "type": "float", "count": 3, "values": [ 1.0, 1.0, 1.0 ] },
+ { "name": "CameraPos", "type": "float", "count": 3, "values": [ 0.0, 0.0, 0.0 ] },
+ { "name": "CloudColor", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ { "name": "FogStart", "type": "float", "count": 1, "values": [ 0.0 ] },
+ { "name": "FogEnd", "type": "float", "count": 1, "values": [ 1.0 ] },
+ { "name": "FogColor", "type": "float", "count": 4, "values": [ 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "AnimationTime", "type": "float", "count": 1, "values": [ 0.0 ] },
+ { "name": "MaxDistance", "type": "float", "count": 1, "values": [ 420.0 ] },
+ { "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
+ { "name": "CloudScale", "type": "float", "count": 1, "values": [ 8.0 ] },
+ { "name": "RenderQuality", "type": "float", "count": 1, "values": [ 0.72 ] },
+ { "name": "StormCount", "type": "int", "count": 1, "values": [ 0 ] },
+ { "name": "DebugMode", "type": "int", "count": 1, "values": [ 0 ] },
+ { "name": "DebugSelectedStorm", "type": "int", "count": 1, "values": [ -1 ] },
+ { "name": "DebugFreeze", "type": "int", "count": 1, "values": [ 0 ] },
+ { "name": "StormPositions", "type": "float", "count": 24, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "StormHeights", "type": "float", "count": 8, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "StormWidths", "type": "float", "count": 8, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "StormSizes", "type": "float", "count": 8, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "StormSpins", "type": "float", "count": 8, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "StormIntensities", "type": "float", "count": 8, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "StormShapes", "type": "float", "count": 8, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] },
+ { "name": "StormProgress", "type": "float", "count": 8, "values": [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ] }
+ ]
+}
diff --git a/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.vsh b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.vsh
new file mode 100644
index 00000000..3501fd78
--- /dev/null
+++ b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_round.vsh
@@ -0,0 +1,11 @@
+#version 150
+
+in vec3 Position;
+in vec2 UV0;
+
+out vec2 texCoord;
+
+void main() {
+ gl_Position = vec4(Position.xy, 0.0, 1.0);
+ texCoord = UV0;
+}
diff --git a/src/main/resources/assets/projectatmosphere/shaders/core/tornado_volume_box.vsh b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_volume_box.vsh
new file mode 100644
index 00000000..414f3d17
--- /dev/null
+++ b/src/main/resources/assets/projectatmosphere/shaders/core/tornado_volume_box.vsh
@@ -0,0 +1,19 @@
+#version 150
+
+uniform mat4 ModelViewMat;
+uniform mat4 ProjMat;
+uniform vec3 VolumeMin;
+uniform vec3 VolumeMax;
+
+in vec3 Position;
+in vec2 UV0;
+
+out vec2 texCoord;
+out vec3 fragPos;
+
+void main() {
+ vec3 worldPos = mix(VolumeMin, VolumeMax, Position);
+ gl_Position = ProjMat * ModelViewMat * vec4(worldPos, 1.0);
+ texCoord = UV0;
+ fragPos = worldPos;
+}
diff --git a/src/main/resources/projectatmosphere.mixins.json b/src/main/resources/projectatmosphere.mixins.json
index 1130e70c..d40bb6b1 100644
--- a/src/main/resources/projectatmosphere.mixins.json
+++ b/src/main/resources/projectatmosphere.mixins.json
@@ -16,9 +16,14 @@
],
"client": [
"CloudMeshGeneratorAccessor",
+ "CloudMeshGeneratorDiagnosticsAccessor",
"CloudMeshGeneratorShaderMixin",
"MixinSandstormDebugBlocker",
"MultiRegionCloudMeshGeneratorMixin",
+ "client.DefaultPipelineTornadoMixin",
+ "client.InstanceableMeshDiagnosticsMixin",
+ "client.SimpleCloudsRendererDiagnosticsMixin",
+ "client.ShaderSupportPipelineTornadoMixin",
"client.LoadingScreenMixin",
"OverwriteDesertSound",
"client.LoadingOverlayMixin",
diff --git a/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/cloud/ClientSideCloudTypeManager.java b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/cloud/ClientSideCloudTypeManager.java
new file mode 100644
index 00000000..0ebe4711
--- /dev/null
+++ b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/cloud/ClientSideCloudTypeManager.java
@@ -0,0 +1,76 @@
+package dev.nonamecrackers2.simpleclouds.client.cloud;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import com.google.common.collect.ImmutableMap;
+
+import dev.nonamecrackers2.simpleclouds.api.common.cloud.weather.WeatherType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudTypeDataManager;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudTypeSource;
+import net.minecraft.resources.ResourceLocation;
+
+public class ClientSideCloudTypeManager implements CloudTypeSource
+{
+ private static final ClientSideCloudTypeManager INSTANCE = new ClientSideCloudTypeManager();
+ private final CloudTypeDataManager dataManager;
+ private Map synced = ImmutableMap.of();
+ private CloudType[] indexed = new CloudType[0];
+
+ private ClientSideCloudTypeManager()
+ {
+ this.dataManager = new CloudTypeDataManager();
+ }
+
+ public CloudTypeDataManager getClientSideDataManager()
+ {
+ return this.dataManager;
+ }
+
+ @Override
+ public CloudType getCloudTypeForId(ResourceLocation id)
+ {
+ return this.getCloudTypes().get(id);
+ }
+
+ @Override
+ public CloudType[] getIndexedCloudTypes()
+ {
+ if (this.indexed.length > 0)
+ return this.indexed;
+ else
+ return this.dataManager.getIndexedCloudTypes();
+ }
+
+ public Map getCloudTypes()
+ {
+ if (!this.synced.isEmpty())
+ return this.synced;
+ else
+ return this.dataManager.getCloudTypes();
+ }
+
+ public void receiveSynced(Map synced, CloudType[] indexed)
+ {
+ this.synced = ImmutableMap.copyOf(synced);
+ this.indexed = indexed;
+ }
+
+ public void clearSynced()
+ {
+ this.synced = ImmutableMap.of();
+ this.indexed = new CloudType[0];
+ }
+
+ public static ClientSideCloudTypeManager getInstance()
+ {
+ return INSTANCE;
+ }
+
+ public static boolean isValidClientSideSingleModeCloudType(@Nullable CloudType type)
+ {
+ return type != null && type.weatherType() == WeatherType.NONE;
+ }
+}
diff --git a/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator.java b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator.java
new file mode 100644
index 00000000..f2ae91fd
--- /dev/null
+++ b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/generator/CloudMeshGenerator.java
@@ -0,0 +1,1149 @@
+package dev.nonamecrackers2.simpleclouds.client.mesh.generator;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Queue;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL31;
+import org.lwjgl.opengl.GL41;
+import org.lwjgl.opengl.GL42;
+import org.lwjgl.opengl.GL43;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Queues;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import dev.nonamecrackers2.simpleclouds.SimpleCloudsMod;
+import dev.nonamecrackers2.simpleclouds.client.mesh.LevelOfDetailOptions;
+import dev.nonamecrackers2.simpleclouds.client.mesh.RendererInitializeResult;
+import dev.nonamecrackers2.simpleclouds.client.mesh.chunk.MeshChunk;
+import dev.nonamecrackers2.simpleclouds.client.mesh.instancing.InstanceableMesh;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetailConfig;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.PreparedChunk;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.BindingManager;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.ShaderStorageBufferObject;
+import dev.nonamecrackers2.simpleclouds.client.shader.compute.ComputeShader;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudInfo;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.mixin.MixinFrustumAccessor;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.util.Mth;
+import net.minecraft.world.phys.AABB;
+
+/**
+ * Abstract mesh generator class that generates a cloud vertex mesh using computer shaders. Implementations are only available on the render thread.
+ *
+ * Use {@link CloudMeshGenerator#init} to initialize the mesh generator. This will initialize all needed buffers. Note
+ * that this is an expensive class and having multiple instances in one environment can cause GPU memory to run out quick (including
+ * available SSBO bindings).
+ *
+ * Use {@link CloudMeshGenerator#tick} each frame to generate the mesh at a fixed interval of frames
+ * (defined by {@link CloudMeshGenerator#setMeshGenInterval}) or use {@link CloudMeshGenerator#generateMesh} to generate
+ * it in a single call.
+ *
+ * Use {@link CloudMeshGenerator#render} to render the currently generated cloud mesh.
+ *
+ * @author nonamecrackers2
+ */
+public abstract class CloudMeshGenerator
+{
+ private static final Logger LOGGER = LogManager.getLogger("simpleclouds/CloudMeshGenerator");
+
+ public static final ResourceLocation MAIN_CUBE_MESH_GENERATOR = SimpleCloudsMod.id("cube_mesh");
+ public static final int MAX_NOISE_LAYERS = 4;
+ public static final int VERTICAL_CHUNK_SPAN = 8;
+ public static final int LOCAL_SIZE = 8;
+ public static final int WORK_SIZE = SimpleCloudsConstants.CHUNK_SIZE / LOCAL_SIZE;
+ public static final int TICKS_UNTIL_FADE_RESET = 120;
+
+ //Opaque
+ public static final int BYTES_PER_SIDE_INFO = 24;
+ public static final int MAX_SIDE_INFO_BUFFER_SIZE = 50331648;
+ public static final String SIDE_INFO_BUFFER_NAME = "SideInfoBuffer";
+ public static final String TOTAL_SIDES_NAME = "TotalSides";
+ public static final String SIDES_PER_CHUNK_NAME = "SidesPerChunk";
+ //Transparent
+ public static final int BYTES_PER_CUBE_INFO = 24;
+ public static final int MAX_TRANSPARENT_CUBE_INFO_BUFFER_SIZE = 50331648;
+ public static final String TRANSPARENT_CUBE_INFO_BUFFER_NAME = "TransparentCubeInfoBuffer";
+ public static final String TRANSPARENT_TOTAL_CUBES_NAME = "TotalTransparentCubes";
+ public static final String TRANSPARENT_CUBES_PER_CHUNK_NAME = "TransparentCubesPerChunk";
+
+ public static final String NOISE_LAYERS_NAME = "NoiseLayers";
+ public static final String LAYER_GROUPINGS_NAME = "LayerGroupings";
+
+ protected final ResourceLocation meshShaderLoc;
+ protected final int shaderType;
+ protected final boolean fadeNearOrigin;
+ protected final boolean shadedClouds;
+ protected final boolean useTransparency;
+ protected final LevelOfDetailConfig lodConfig;
+ protected final boolean useFixedMeshDataSectionSize;
+ protected @Nullable List chunks;
+ protected final List completedGenTasks = Lists.newArrayList();
+ protected final Queue chunkGenTasks = Queues.newArrayDeque();
+ protected final Supplier meshGenIntervalCalculator;
+ protected int meshGenInterval = 1;
+ protected int tasksPerTick;
+ protected @Nullable ComputeShader shader;
+
+ protected @Nullable InstanceableMesh sideMesh;
+ protected @Nullable InstanceableMesh cubeMesh;
+
+ // Left is for opaque geometry, right is for transparent
+ protected Pair meshGenStatus = Pair.of(CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED, CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED);
+ protected float scrollX;
+ protected float scrollY;
+ protected float scrollZ;
+ protected boolean testFacesFacingAway;
+ private float fadeStart;
+ private float fadeEnd;
+ private float cullDistance;
+ private int transparencyDistance;
+
+ private int opaqueBufferSize;
+ private int opaqueBufferBytesUsed;
+ private int transparentBufferSize;
+ private int transparentBufferBytesUsed;
+ private int opaqueBytesPerChunk;
+ private int transparentBytesPerChunk;
+
+ /**
+ * Creates a cloud mesh generator, but does not initialize it for generating (use {@link CloudMeshGenerator#init})
+ *
+ * @param meshShaderLoc
+ * The location of the cloud mesh generator compute shader
+ * @param lodConfig
+ * A level of detail configuration
+ * @param meshGenInterval
+ * The frame interval at which the generate the cloud mesh
+ */
+ public CloudMeshGenerator(ResourceLocation meshShaderLoc, int shaderType, boolean fadeNearOrigin, boolean shadedClouds, LevelOfDetailConfig lodConfig, Supplier meshGenIntervalCalculator, boolean useTransparency, boolean fixedMeshDataSectionSize)
+ {
+ this.meshShaderLoc = meshShaderLoc;
+ this.shaderType = shaderType;
+ this.fadeNearOrigin = fadeNearOrigin;
+ this.shadedClouds = shadedClouds;
+ this.useFixedMeshDataSectionSize = fixedMeshDataSectionSize;
+
+ this.lodConfig = lodConfig;
+ this.meshGenIntervalCalculator = meshGenIntervalCalculator;
+ this.useTransparency = useTransparency;
+
+ float maxRadius = this.getCloudAreaMaxRadius();
+ this.fadeStart = 0.9F * maxRadius;
+ this.fadeEnd = maxRadius;
+ this.transparencyDistance = (int)maxRadius / 2;
+ }
+
+ public boolean fadeNearOriginEnabled()
+ {
+ return this.fadeNearOrigin;
+ }
+
+ public boolean shadedCloudsEnabled()
+ {
+ return this.shadedClouds;
+ }
+
+ public boolean transparencyEnabled()
+ {
+ return this.useTransparency;
+ }
+
+ public boolean usesFixedMeshDataSectionSize()
+ {
+ return this.useFixedMeshDataSectionSize;
+ }
+
+ public LevelOfDetailConfig getLodConfig()
+ {
+ return this.lodConfig;
+ }
+
+ /**
+ * Specifies if faces not facing the camera should be tested during
+ * mesh generation on the GPU for whether they should be generated or not.
+ *
+ * Enabling can improve performance at the cost of some visual artifacts
+ * or an incomplete cloud mesh
+ *
+ * @param flag
+ * @return
+ */
+ public CloudMeshGenerator setTestFacesFacingAway(boolean flag)
+ {
+ this.testFacesFacingAway = flag;
+ return this;
+ }
+
+ /**
+ * Sets the fade start and end distances as decimal percentages
+ *
+ * @param fadeStart
+ * @param fadeEnd
+ */
+ public CloudMeshGenerator setFadeDistances(float fadeStart, float fadeEnd)
+ {
+ float fs = fadeStart;
+ float fe = fadeEnd;
+ if (fs > fe)
+ {
+ fs = fadeEnd;
+ fe = fadeStart;
+ }
+ this.fadeStart = fs * (float)this.getCloudAreaMaxRadius();
+ this.fadeEnd = fe * (float)this.getCloudAreaMaxRadius();
+ return this;
+ }
+
+ public CloudMeshGenerator setTransparencyRenderDistance(float percentage)
+ {
+ this.transparencyDistance = Mth.floor(percentage * (float)this.getCloudAreaMaxRadius());
+ return this;
+ }
+
+ public float getFadeStart()
+ {
+ return this.fadeStart;
+ }
+
+ public float getFadeEnd()
+ {
+ return this.fadeEnd;
+ }
+
+ public int getCloudAreaMaxRadius()
+ {
+ return this.lodConfig.getEffectiveChunkSpan() * WORK_SIZE * LOCAL_SIZE / 2;
+ }
+
+ public void setCullDistance(float dist)
+ {
+ if (dist <= 0.0F)
+ throw new IllegalArgumentException("Cull distance must be greater than zero");
+ this.cullDistance = dist;
+ }
+
+ public void disableCullDistance()
+ {
+ this.cullDistance = 0.0F;
+ }
+
+ public void setScroll(float x, float y, float z)
+ {
+ this.scrollX = x;
+ this.scrollY = y;
+ this.scrollZ = z;
+ }
+
+ public Pair getMeshGenStatus()
+ {
+ return this.meshGenStatus;
+ }
+
+ public @Nullable InstanceableMesh getSideMesh()
+ {
+ return this.sideMesh;
+ }
+
+ public @Nullable InstanceableMesh getCubeMesh()
+ {
+ return this.cubeMesh;
+ }
+
+ public int getOpaqueBufferSize()
+ {
+ return this.opaqueBufferSize;
+ }
+
+ public int getOpaqueBufferBytesUsed()
+ {
+ return this.opaqueBufferBytesUsed;
+ }
+
+ public int getTransparentBufferSize()
+ {
+ return this.transparentBufferSize;
+ }
+
+ public int getTransparentBufferBytesUsed()
+ {
+ return this.transparentBufferBytesUsed;
+ }
+
+ public int getOpaqueBytesPerChunk()
+ {
+ return this.opaqueBytesPerChunk;
+ }
+
+ public int getTransparentBytesPerChunk()
+ {
+ return this.transparentBytesPerChunk;
+ }
+
+ public int getTotalMeshChunks()
+ {
+ if (this.chunks == null)
+ return 0;
+ return this.chunks.size();
+ }
+
+ public int getMeshGenInterval()
+ {
+ return this.meshGenInterval;
+ }
+
+ public void close()
+ {
+ RenderSystem.assertOnRenderThreadOrInit();
+
+ this.opaqueBufferBytesUsed = 0;
+ this.opaqueBufferSize = 0;
+ this.opaqueBytesPerChunk = 0;
+ this.transparentBufferBytesUsed = 0;
+ this.transparentBufferSize = 0;
+ this.transparentBytesPerChunk = 0;
+
+ GL42.glMemoryBarrier(GL42.GL_ALL_BARRIER_BITS);
+ this.chunkGenTasks.clear();
+ this.completedGenTasks.clear();
+
+ if (this.shader != null)
+ this.shader.close();
+ this.shader = null;
+
+ if (this.chunks != null)
+ {
+ for (MeshChunk chunk : this.chunks)
+ chunk.destroy();
+ this.chunks = null;
+ }
+
+ if (this.sideMesh != null)
+ {
+ this.sideMesh.destroy();
+ this.sideMesh = null;
+ }
+
+ if (this.cubeMesh != null)
+ {
+ this.cubeMesh.destroy();
+ this.cubeMesh = null;
+ }
+ }
+
+ public boolean canRender()
+ {
+ return this.chunks != null;
+ }
+
+ public final RendererInitializeResult init(ResourceManager manager)
+ {
+ RendererInitializeResult.Builder builder = RendererInitializeResult.builder();
+
+ if (!RenderSystem.isOnRenderThreadOrInit())
+ return builder.errorUnknown(new IllegalStateException("Init not called on render thread"), "Mesh Generator; Head").build();
+
+ this.opaqueBufferBytesUsed = 0;
+ this.opaqueBufferSize = 0;
+ this.opaqueBytesPerChunk = 0;
+ this.transparentBufferBytesUsed = 0;
+ this.transparentBufferSize = 0;
+ this.transparentBytesPerChunk = 0;
+
+ GL42.glMemoryBarrier(GL42.GL_ALL_BARRIER_BITS);
+ this.chunkGenTasks.clear();
+ this.completedGenTasks.clear();
+
+ LOGGER.debug("Beginning mesh generator initialization");
+
+ if (this.shader != null)
+ {
+ LOGGER.debug("Freeing mesh compute shader");
+ this.shader.close();
+ this.shader = null;
+ }
+
+ if (this.chunks != null)
+ {
+ for (MeshChunk chunk : this.chunks)
+ chunk.destroy();
+ this.chunks = null;
+ }
+
+ try
+ {
+ LOGGER.debug("Creating mesh compute shader...");
+ this.shader = this.createShader(manager);
+ this.setupShader();
+ }
+ catch (IOException e)
+ {
+ //LOGGER.warn("Failed to load compute shader", e);
+ builder.errorCouldNotLoadMeshScript(e, "Mesh Generator; Compute Shader");
+ }
+ catch (Exception e)
+ {
+ builder.errorRecommendations(e, "Mesh Generator; Compute Shader");
+ }
+
+ try
+ {
+ this.initExtra(manager);
+ }
+ catch (Exception e)
+ {
+ builder.errorUnknown(e, "Init Extra");
+ }
+
+ List preparedChunks = this.getLodConfig().getPreparedChunks();
+ ImmutableList.Builder meshChunks = ImmutableList.builder();
+ int totalPreparedChunks = preparedChunks.size();
+ this.opaqueBytesPerChunk = Mth.ceil(this.opaqueBufferSize / totalPreparedChunks);
+ this.transparentBytesPerChunk = Mth.ceil(this.transparentBufferSize / totalPreparedChunks);
+ if (!this.useFixedMeshDataSectionSize)
+ {
+ this.opaqueBytesPerChunk *= 4;
+ this.transparentBytesPerChunk *= 4;
+ }
+ int maxOpaqueElements = Mth.floor((float)this.opaqueBytesPerChunk / (float)BYTES_PER_SIDE_INFO);
+ int maxTransparentElements = Mth.floor((float)this.transparentBytesPerChunk / (float)BYTES_PER_CUBE_INFO);
+ int opaqueElementOffset = 0;
+ int transparentElementOffset = 0;
+ for (PreparedChunk chunk : preparedChunks)
+ {
+ meshChunks.add(new MeshChunk(chunk, maxOpaqueElements, opaqueElementOffset, BYTES_PER_SIDE_INFO, maxTransparentElements, transparentElementOffset, BYTES_PER_CUBE_INFO, this.useTransparency));
+ opaqueElementOffset += maxOpaqueElements;
+ transparentElementOffset += maxTransparentElements;
+ }
+ this.chunks = meshChunks.build();
+
+ LOGGER.debug("Opaque buffer size: {} bytes, transparent buffer size: {} bytes", this.opaqueBufferSize, this.transparentBufferSize);
+
+ if (this.sideMesh != null)
+ this.sideMesh.destroy();
+ this.sideMesh = InstanceableMesh.defaultSide();
+
+ if (this.cubeMesh != null)
+ this.cubeMesh.destroy();
+ this.cubeMesh = InstanceableMesh.defaultCube();
+
+ BindingManager.printDebug();
+
+ LOGGER.debug("Finished initializing mesh generator");
+
+ return builder.build();
+ }
+
+ protected ComputeShader createShader(ResourceManager manager) throws IOException
+ {
+ ImmutableMap parameters = ImmutableMap.of(
+ "TYPE", String.valueOf(this.shaderType),
+ "FADE_NEAR_ORIGIN", this.fadeNearOrigin ? "1" : "0",
+ "STYLE", this.shadedClouds ? "1" : "0",
+ "TRANSPARENCY", this.useTransparency ? "1" : "0",
+ "FIXED_SECTION_SIZE", this.useFixedMeshDataSectionSize ? "1" : "0"
+ );
+ return ComputeShader.loadShader(this.meshShaderLoc, manager, LOCAL_SIZE, LOCAL_SIZE, LOCAL_SIZE, parameters);
+ }
+
+ protected void setupShader()
+ {
+ this.opaqueBufferSize = this.createBuffers(
+ TOTAL_SIDES_NAME,
+ SIDES_PER_CHUNK_NAME,
+ SIDE_INFO_BUFFER_NAME,
+ MAX_SIDE_INFO_BUFFER_SIZE * (this.useFixedMeshDataSectionSize ? 4 : 1)
+ );
+
+ if (this.useTransparency)
+ {
+ this.transparentBufferSize = this.createBuffers(
+ TRANSPARENT_TOTAL_CUBES_NAME,
+ TRANSPARENT_CUBES_PER_CHUNK_NAME,
+ TRANSPARENT_CUBE_INFO_BUFFER_NAME,
+ MAX_TRANSPARENT_CUBE_INFO_BUFFER_SIZE * (this.useFixedMeshDataSectionSize ? 4 : 1)
+ );
+ }
+
+ this.shader.forUniform("TotalLodLevels", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.lodConfig.getLods().length);
+ });
+
+ this.uploadFadeData();
+ }
+
+ private void uploadFadeData()
+ {
+ if (this.shader == null || !this.shader.isValid())
+ return;
+
+ this.shader.forUniform("TransparencyDistance", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.transparencyDistance);
+ });
+ this.shader.forUniform("FadeStart", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, this.fadeStart);
+ });
+ this.shader.forUniform("FadeEnd", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, this.fadeEnd);
+ });
+ }
+
+ private int createBuffers(String totalCounterName, String countPerChunkName, String elementInfoBufferName, int maxSize)
+ {
+ if (!this.useFixedMeshDataSectionSize)
+ {
+ ShaderStorageBufferObject totalCountBuffer = this.shader.createAndBindSSBO(totalCounterName, GL15.GL_DYNAMIC_COPY);
+ totalCountBuffer.allocateBuffer(4);
+ totalCountBuffer.writeData(b -> {
+ b.putInt(0, 0);
+ }, 4, false);
+ }
+
+ int bufferSize = this.shader.createAndBindSSBO(elementInfoBufferName, GL15.GL_DYNAMIC_COPY).allocateBuffer(maxSize);
+
+ int totalChunks = this.getLodConfig().getPreparedChunks().size();
+ int countPerChunkBufferSize = totalChunks * 4;
+ ShaderStorageBufferObject countPerChunkBuffer = this.shader.createAndBindSSBO(countPerChunkName, GL15.GL_DYNAMIC_COPY);
+ countPerChunkBuffer.allocateBuffer(countPerChunkBufferSize);
+ countPerChunkBuffer.writeData(b ->
+ {
+ for (int i = 0; i < totalChunks; i++)
+ b.putInt(0);
+ b.rewind();
+ }, countPerChunkBufferSize, false);
+
+ return bufferSize;
+ }
+
+ protected void initExtra(ResourceManager manager) throws IOException {}
+
+ /**
+ * Generates the entire cloud mesh at the origin at once
+ */
+ public void generateMesh()
+ {
+ RenderSystem.assertOnRenderThread();
+
+ if (this.shader == null || !this.shader.isValid())
+ return;
+
+ this.prepareMeshGen(0.0D, 0.0D, 0.0D, 0.0F, 0.0F, null, 1, 1.0F);
+
+ if (!this.chunkGenTasks.isEmpty())
+ this.doMeshGenning(this.chunkGenTasks.size());
+
+ this.meshGenStatus = this.finalizeMeshGen();
+ this.completedGenTasks.clear();
+ }
+
+ public void worldTick()
+ {
+ if (this.chunks != null)
+ this.chunks.forEach(MeshChunk::tick);
+ }
+
+ /**
+ * Generates the cloud mesh on a per-frame basis
+ *
+ * @param originX
+ * @param originY
+ * @param originZ
+ * @param frustum
+ */
+ public void genTick(double originX, double originY, double originZ, @Nullable Frustum frustum, float partialTick)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ if (this.shader == null || !this.shader.isValid())
+ return;
+
+ float chunkSize = (float)SimpleCloudsConstants.CHUNK_SIZE;
+ float meshGenOffsetX = (float)Mth.floor(originX / chunkSize) * chunkSize;
+ float meshGenOffsetZ = (float)Mth.floor(originZ / chunkSize) * chunkSize;
+
+ if (this.chunkGenTasks.isEmpty()) //If we have no chunk gen tasks
+ {
+ this.meshGenStatus = this.finalizeMeshGen(); //Split the combined mesh data from the GPU, and store them in the VBOs for each chunk that was generated
+ this.completedGenTasks.clear(); //Clear the chunk gen tasks
+
+ //Prepare the next batch of chunks to generate meshes for
+ this.meshGenInterval = this.meshGenIntervalCalculator.get();
+ if (this.meshGenInterval <= 0)
+ throw new RuntimeException("Mesh gen interval is <= 0");
+ this.tasksPerTick = this.prepareMeshGen(originX, originY, originZ, meshGenOffsetX, meshGenOffsetZ, frustum, this.meshGenInterval, partialTick);
+ }
+ else
+ {
+ this.onOffGen();
+ }
+
+ //If there are mesh gen tasks, we do mesh genning
+ if (!this.chunkGenTasks.isEmpty())
+ this.doMeshGenning(this.tasksPerTick);
+ }
+
+ private static CloudMeshGenerator.MeshGenStatus fixedIterateAndCopyToChunkBuffer(int copyBufferId, int copyBufferSizeBytes, Collection chunks, Function byteOffsetPerChunk, Function chunkBufferId, Function bytesToCopyPerChunk, Function bufferSizeBytesPerChunk)
+ {
+ CloudMeshGenerator.MeshGenStatus result = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_READ_BUFFER, copyBufferId);
+
+ for (MeshChunk chunk : chunks)
+ {
+ int bytesToCopy = bytesToCopyPerChunk.apply(chunk);
+ if (bytesToCopy > 0)
+ {
+ int maxSize = bufferSizeBytesPerChunk.apply(chunk);
+ if (bytesToCopy > maxSize) // Too many bytes to go in to the chunk mesh buffer
+ {
+ bytesToCopy = maxSize;
+ result = CloudMeshGenerator.MeshGenStatus.CHUNK_OVERFLOW;
+ }
+
+ int byteOffset = byteOffsetPerChunk.apply(chunk);
+ if (byteOffset + bytesToCopy > copyBufferSizeBytes) // TODO: Account for this overflow using mesh gen status
+ {
+ //TODO: Make sure this uses multiples of the size of a single element to avoid cutting off a single element
+ bytesToCopy = copyBufferSizeBytes - byteOffset;
+ if (bytesToCopy <= 0)
+ continue;
+ }
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, chunkBufferId.apply(chunk));
+ GL31.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, byteOffset, 0, bytesToCopy);
+ }
+ }
+
+ return result;
+ }
+
+ private static CloudMeshGenerator.MeshGenStatus packedIterateAndCopyToChunkBuffer(int copyBufferId, int copyBufferSizeBytes, Collection chunks, Function chunkBufferId, Function bytesToCopyPerChunk, Function bufferSizeBytesPerChunk)
+ {
+ CloudMeshGenerator.MeshGenStatus result = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_READ_BUFFER, copyBufferId);
+
+ int currentBytes = 0;
+ for (MeshChunk chunk : chunks)
+ {
+ int totalBytes = bytesToCopyPerChunk.apply(chunk);
+ if (totalBytes > 0) //If the chunk has data that needs copying over
+ {
+ int lastBytesOffset = totalBytes;
+ int maxSize = bufferSizeBytesPerChunk.apply(chunk);
+ if (lastBytesOffset > maxSize) //Make sure we don't go over the maximum the chunk buffer can hold
+ {
+ lastBytesOffset = maxSize;
+ result = CloudMeshGenerator.MeshGenStatus.CHUNK_OVERFLOW;
+ }
+ boolean stop = false;
+ if (currentBytes + lastBytesOffset > copyBufferSizeBytes) //If the the byte offset will go over the size of the copy buffer, clamp
+ {
+ lastBytesOffset = copyBufferSizeBytes - currentBytes;
+ if (lastBytesOffset <= 0) // If it becomes negative however, we don't want to attempt to copy data over
+ return CloudMeshGenerator.MeshGenStatus.MESH_POOL_OVERFLOW;
+ stop = true; // After copying this data over we will stop, since there is no more space in the copy buffer to read data from
+ }
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, chunkBufferId.apply(chunk));
+ GL31.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, currentBytes, 0, lastBytesOffset);
+
+ currentBytes += totalBytes;
+
+ if (stop)
+ return CloudMeshGenerator.MeshGenStatus.MESH_POOL_OVERFLOW;
+ }
+ }
+
+ return result;
+ }
+
+ protected Pair finalizeMeshGen()
+ {
+ if (this.shader == null || !this.shader.isValid() || this.chunks == null)
+ return Pair.of(CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED, CloudMeshGenerator.MeshGenStatus.NOT_INITIALIZED);
+
+ if (this.completedGenTasks.isEmpty())
+ return Pair.of(CloudMeshGenerator.MeshGenStatus.NO_TASKS, CloudMeshGenerator.MeshGenStatus.NO_TASKS);
+
+ RenderSystem.assertOnRenderThread();
+
+ GL42.glMemoryBarrier(GL43.GL_SHADER_STORAGE_BARRIER_BIT);
+
+ CloudMeshGenerator.MeshGenStatus opaqueResult = CloudMeshGenerator.MeshGenStatus.NORMAL;
+ CloudMeshGenerator.MeshGenStatus transparentResult = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ opaqueResult = this.copyMeshData(
+ TOTAL_SIDES_NAME,
+ SIDES_PER_CHUNK_NAME,
+ SIDE_INFO_BUFFER_NAME,
+ MeshChunk::getOpaqueBuffers,
+ BYTES_PER_SIDE_INFO,
+ this.opaqueBufferSize
+ );
+
+ if (this.useTransparency)
+ {
+ transparentResult = this.copyMeshData(
+ TRANSPARENT_TOTAL_CUBES_NAME,
+ TRANSPARENT_CUBES_PER_CHUNK_NAME,
+ TRANSPARENT_CUBE_INFO_BUFFER_NAME,
+ c -> c.getTransparentBuffers().get(),
+ BYTES_PER_CUBE_INFO,
+ this.transparentBufferSize
+ );
+ }
+
+ this.opaqueBufferBytesUsed = 0;
+ this.transparentBufferBytesUsed = 0;
+ for (MeshChunk chunk : this.chunks)
+ {
+ this.opaqueBufferBytesUsed += chunk.getOpaqueBuffers().getElementCount() * BYTES_PER_SIDE_INFO;
+ chunk.getTransparentBuffers().ifPresent(bufferSet -> {
+ this.transparentBufferBytesUsed += bufferSet.getElementCount() * BYTES_PER_CUBE_INFO;
+ });
+ }
+
+ return Pair.of(opaqueResult, transparentResult);
+ }
+
+ private CloudMeshGenerator.MeshGenStatus copyMeshData(String totalCountBufferName, String countPerChunkBufferName, String elementBufferName, Function bufferSetFunction, int bytesPerElement, int elementBufferSize)
+ {
+ CloudMeshGenerator.MeshGenStatus status = CloudMeshGenerator.MeshGenStatus.NORMAL;
+
+ if (!this.useFixedMeshDataSectionSize)
+ {
+ //Get the total amount of sides and indices across all chunks and reset
+ this.shader.getShaderStorageBuffer(totalCountBufferName).writeData(b -> {
+ b.putInt(0, 0);
+ }, 4, true);
+ }
+
+ //Get the amount of total sides each chunk has and reset each counter
+ this.shader.getShaderStorageBuffer(countPerChunkBufferName).readWriteData(buffer ->
+ {
+ for (CloudMeshGenerator.ChunkGenTask gennedChunk : this.completedGenTasks)
+ {
+ MeshChunk.BufferSet bufferSet = bufferSetFunction.apply(gennedChunk.chunk());
+ int index = gennedChunk.index() * 4;
+ int count = buffer.getInt(index);
+ bufferSet.setTotalElementCount(count);
+ buffer.putInt(index, 0);
+ }
+ }, this.chunks.size() * 4);
+
+ List completedChunks = this.completedGenTasks.stream().map(CloudMeshGenerator.ChunkGenTask::chunk).toList();
+
+ int elementBufferId = this.shader.getShaderStorageBuffer(elementBufferName).getId();
+ if (this.useFixedMeshDataSectionSize)
+ status = fixedIterateAndCopyToChunkBuffer(elementBufferId, elementBufferSize, completedChunks, bufferSetFunction.andThen(b -> b.getElementOffset() * bytesPerElement), bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferId), bufferSetFunction.andThen(c -> c.getElementCount() * bytesPerElement), bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferSize));
+ else
+ status = packedIterateAndCopyToChunkBuffer(elementBufferId, elementBufferSize, completedChunks, bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferId), bufferSetFunction.andThen(c -> c.getElementCount() * bytesPerElement), bufferSetFunction.andThen(MeshChunk.BufferSet::getBufferSize));
+
+ GlStateManager._glBindBuffer(GL31.GL_COPY_READ_BUFFER, 0);
+ GlStateManager._glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, 0);
+
+ return status;
+ }
+
+ /**
+ * Queues a list of chunk gen tasks for each chunk in this mesh generator
+ *
+ * @param meshGenOffsetX
+ * @param meshGenOffsetZ
+ * @param frustum
+ * Culling frustum, null for no culling
+ * @param genInterval
+ * How many frames mesh genning should take
+ * @return
+ */
+ protected int prepareMeshGen(double originX, double originY, double originZ, float meshGenOffsetX, float meshGenOffsetZ, @Nullable Frustum frustum, int genInterval, float partialTick)
+ {
+ this.shader.forUniform("Scroll", (id, loc) -> {
+ GL41.glProgramUniform3f(id, loc, this.scrollX, this.scrollY, this.scrollZ);
+ });
+ this.shader.forUniform("Wiggle", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, (this.scrollX + this.scrollY + this.scrollZ) / 5.0F);
+ });
+ this.shader.forUniform("Origin", (id, loc) -> {
+ GL41.glProgramUniform3f(id, loc, (float)originX, (float)originY, (float)originZ);
+ });
+ this.shader.forUniform("TestFacesFacingAway", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.testFacesFacingAway ? 1 : 0);
+ });
+ this.uploadFadeData();
+
+ int chunkCount = 0;
+ for (int i = 0; i < this.chunks.size(); i++)
+ {
+ if (this.queueChunkMeshGenTaskOrClear(this.chunks.get(i), i, meshGenOffsetX, meshGenOffsetZ, frustum))
+ chunkCount++;
+ }
+ return Mth.ceil((float)chunkCount / (float)genInterval);
+ }
+
+ protected void onOffGen()
+ {
+ //We read these SSBOs here to avoid weird frame spikes when in fullscreen V-Sync, not sure why it happens
+ if (!this.useFixedMeshDataSectionSize)
+ this.shader.getShaderStorageBuffer(TOTAL_SIDES_NAME).readWriteData(b -> {}, 4);
+ this.shader.getShaderStorageBuffer(SIDES_PER_CHUNK_NAME).readWriteData(buffer -> {}, this.chunks.size() * 4);
+ if (this.useTransparency)
+ {
+ if (!this.useFixedMeshDataSectionSize)
+ this.shader.getShaderStorageBuffer(TRANSPARENT_TOTAL_CUBES_NAME).readWriteData(b -> {}, 4);
+ this.shader.getShaderStorageBuffer(TRANSPARENT_CUBES_PER_CHUNK_NAME).readWriteData(buffer -> {}, this.chunks.size() * 4);
+ }
+ }
+
+ /**
+ * Queues a given chunk for mesh genning or clears it if empty
+ *
+ * @param chunk
+ * The given {@link MeshChunk} to generate a mesh for
+ * @param chunkIndex
+ * The index of the mesh chunk in {@code this.chunks}
+ * @param meshGenOffsetX
+ * @param meshGenOffsetZ
+ * @param frustum
+ * For frustum culling, null for no culling
+ * @return
+ */
+ protected boolean queueChunkMeshGenTaskOrClear(MeshChunk chunk, int chunkIndex, float meshGenOffsetX, float meshGenOffsetZ, @Nullable Frustum frustum)
+ {
+ PreparedChunk chunkInfo = chunk.getChunkInfo();
+ AABB bounds = chunkInfo.bounds();
+ float minX = (float)bounds.minX + meshGenOffsetX;
+ float minZ = (float)bounds.minZ + meshGenOffsetZ;
+ float maxX = (float)bounds.maxX + meshGenOffsetX;
+ float maxZ = (float)bounds.maxZ + meshGenOffsetZ;
+
+ if (frustum == null || ((MixinFrustumAccessor)frustum).simpleclouds$cubeInFrustum(minX, bounds.minY, minZ, maxX, bounds.maxY, maxZ))
+ {
+ double nearestCornerX = Math.max(Math.max(bounds.minX, -bounds.maxX), 0.0D);
+ double nearestCornerZ = Math.max(Math.max(bounds.minZ, -bounds.maxZ), 0.0D);
+ double dist = Math.sqrt(nearestCornerX * nearestCornerX + nearestCornerZ * nearestCornerZ);
+
+ if (this.cullDistance <= 0.0F || dist < this.cullDistance)
+ {
+ CloudMeshGenerator.ChunkGenSettings settings = this.determineChunkGenSettings(minX, minZ, maxX, maxZ);
+ if (settings.skipChunk())
+ {
+ chunk.clearChunk();
+ return false;
+ }
+ this.chunkGenTasks.add(new CloudMeshGenerator.ChunkGenTask(chunk, minX, (float)bounds.minY, minZ, maxX, (float)bounds.maxY, maxZ, chunkIndex, minX, 0.0F, minZ, settings.minimumHeight(), settings.maximumHeight()));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected abstract CloudMeshGenerator.ChunkGenSettings determineChunkGenSettings(float minX, float minZ, float maxX, float maxZ);
+
+ /**
+ * Does mesh generating for a given amount of chunks defined by tasksPerTick
+ *
+ * @param tasksPerTick
+ */
+ protected void doMeshGenning(int tasksPerTick)
+ {
+ for (int i = 0; i < tasksPerTick; i++)
+ {
+ CloudMeshGenerator.ChunkGenTask task = this.chunkGenTasks.poll();
+ if (task != null)
+ {
+ this.generateChunk(task);
+ this.updateMeshChunkAfterGeneration(task.chunk(), task);
+ this.completedGenTasks.add(task);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ protected void updateMeshChunkAfterGeneration(MeshChunk chunk, CloudMeshGenerator.ChunkGenTask task)
+ {
+ chunk.setBounds(task.minX(), task.minY(), task.minZ(), task.maxX(), task.maxY(), task.maxZ());
+ chunk.setHeights(task.startY(), task.endY());
+ chunk.resetLastGenTime();
+ }
+
+ /**
+ * Generates a given chunk, or completes a chunk gen task
+ *
+ * @param task
+ * @param scale
+ * @param globalOffsetX
+ * @param globalOffsetZ
+ */
+ protected void generateChunk(CloudMeshGenerator.ChunkGenTask task)
+ {
+ PreparedChunk chunkInfo = task.chunk().getChunkInfo();
+
+ int lodScale = chunkInfo.lodScale();
+ int lowestY = task.startY();
+ int height = Mth.ceil((float)(task.endY() - lowestY) / (float)lodScale);
+ int localHeightInvocations = Mth.ceil((float)height / (float)LOCAL_SIZE);
+
+ if (localHeightInvocations > 0)
+ {
+ this.shader.forUniform("ChunkIndex", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, task.index());
+ });
+ this.shader.forUniform("LodLevel", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, chunkInfo.lodLevel());
+ });
+ this.shader.forUniform("RenderOffset", (id, loc) -> {
+ GL41.glProgramUniform3f(id, loc, task.x(), task.y() + lowestY, task.z());
+ });
+ this.shader.forUniform("Scale", (id, loc) -> {
+ GL41.glProgramUniform1f(id, loc, lodScale);
+ });
+ this.shader.forUniform("DoNotOccludeSide", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, chunkInfo.noOcclusionDirectionIndex());
+ });
+ if (this.useFixedMeshDataSectionSize)
+ {
+ this.shader.forUniform("OpaqueMeshDataOffset", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, task.chunk().getOpaqueBuffers().getElementOffset());
+ });
+
+ task.chunk().getTransparentBuffers().ifPresent(bufferSet ->
+ {
+ this.shader.forUniform("TransparentMeshDataOffset", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, bufferSet.getElementOffset());
+ });
+ });
+ }
+
+ this.shader.dispatch(WORK_SIZE, localHeightInvocations, WORK_SIZE, false);
+ if (!this.useFixedMeshDataSectionSize)
+ GL42.glMemoryBarrier(GL43.GL_SHADER_STORAGE_BARRIER_BIT);
+ }
+ }
+
+ public void forRenderableMeshChunks(@Nullable Frustum frustum, Function bufferSetFunction, BiConsumer function)
+ {
+ this.forRenderableMeshChunks(frustum, bufferSetFunction, function, false);
+ }
+
+ public void forRenderableMeshChunks(@Nullable Frustum frustum, Function bufferSetFunction, BiConsumer function, boolean updateFade)
+ {
+ for (MeshChunk chunk : this.chunks)
+ {
+ MeshChunk.BufferSet bufferSet = bufferSetFunction.apply(chunk);
+ if (bufferSet.getElementCount() > 0)
+ {
+ if (updateFade && chunk.getTicksSinceLastGen() > TICKS_UNTIL_FADE_RESET)
+ {
+ chunk.resetAlpha();
+ chunk.setFadeEnabled(false);
+ }
+
+ boolean render = true;
+ if (frustum != null)
+ render = ((MixinFrustumAccessor)frustum).simpleclouds$cubeInFrustum(chunk.getBoundsMinX(), chunk.getBoundsMinY(), chunk.getBoundsMinZ(), chunk.getBoundsMaxX(), chunk.getBoundsMaxY(), chunk.getBoundsMaxZ());
+
+ if (render)
+ {
+ PreparedChunk chunkInfo = chunk.getChunkInfo();
+ AABB bounds = chunkInfo.bounds();
+ double nearestCornerX = Math.max(Math.max(bounds.minX, -bounds.maxX), 0.0D);
+ double nearestCornerZ = Math.max(Math.max(bounds.minZ, -bounds.maxZ), 0.0D);
+ double dist = Math.sqrt(nearestCornerX * nearestCornerX + nearestCornerZ * nearestCornerZ);
+ if (this.cullDistance <= 0.0F || this.cullDistance > dist)
+ {
+ if (updateFade)
+ chunk.setFadeEnabled(true);
+ function.accept(chunk, bufferSet);
+ }
+ }
+ }
+ }
+ }
+
+ public void fillReport(CrashReportCategory category)
+ {
+ category.setDetail("Shader Type", this.shaderType);
+ category.setDetail("Shaded Clouds", this.shadedClouds);
+ category.setDetail("Transparency Enabled", this.useTransparency);
+ category.setDetail("Fade Near Origin", this.fadeNearOrigin);
+ category.setDetail("Compute Shader", this.shader);
+ category.setDetail("Level Of Details", 1 + this.lodConfig.getLods().length);
+ category.setDetail("Generation Frame Interval", this.meshGenInterval);
+ category.setDetail("Total Prepared Chunks", this.lodConfig.getPreparedChunks().size());
+ category.setDetail("Tasks Per Frame", this.tasksPerTick);
+ category.setDetail("Scroll", String.format("X: %s, Y: %s, Z: %s", this.scrollX, this.scrollY, this.scrollZ));
+ category.setDetail("Total Mesh Chunks", this.chunks != null ? this.chunks.size() : "null");
+ category.setDetail("Mesh Gen Status", this.meshGenStatus);
+ category.setDetail("Test Occluded Faces", this.testFacesFacingAway);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[shader_name=%s]", this.getClass().getSimpleName(), this.meshShaderLoc);
+ }
+
+ protected static CloudMeshGenerator.ChunkGenSettings skip()
+ {
+ return new CloudMeshGenerator.ChunkGenSettings(true, 0, 0);
+ }
+
+ protected static CloudMeshGenerator.ChunkGenSettings heights(int min, int max)
+ {
+ return new CloudMeshGenerator.ChunkGenSettings(false, min, max);
+ }
+
+ public static CloudMeshGenerator.Builder builder()
+ {
+ return new CloudMeshGenerator.Builder();
+ }
+
+ protected static record ChunkGenSettings(boolean skipChunk, int minimumHeight, int maximumHeight) {}
+
+ protected static record ChunkGenTask(MeshChunk chunk, float minX, float minY, float minZ, float maxX, float maxY, float maxZ, int index, float x, float y, float z, int startY, int endY) {}
+
+ public static enum MeshGenStatus
+ {
+ NOT_INITIALIZED("Not initialized", true),
+ NO_TASKS("No tasks", false),
+ NORMAL("Normal", false),
+ MESH_POOL_OVERFLOW("Mesh pool overflow", true),
+ CHUNK_OVERFLOW("Chunk overflow", true);
+
+ private String name;
+ private boolean isErroneous;
+
+ private MeshGenStatus(String name, boolean isErroneous)
+ {
+ this.name = name;
+ this.isErroneous = isErroneous;
+ }
+
+ public String getName()
+ {
+ return this.name;
+ }
+
+ public boolean isErroneous()
+ {
+ return this.isErroneous;
+ }
+ }
+
+ public static class Builder
+ {
+ private boolean fadeNearOrigin;
+ private boolean shadedClouds = true;
+ private LevelOfDetailConfig lodConfig = LevelOfDetailOptions.HIGH.getConfig();
+ private Supplier meshGenIntervalCalculator = () -> 5;
+ private boolean useTransparency = true;
+ private boolean fixedMeshDataSectionSize;
+ private float fadeStart = 0.5F;
+ private float fadeEnd = 1.0F;
+ private boolean testFacesFacingAway = false;
+
+ private Builder() {}
+
+ public Builder fadeNearOrigin(boolean flag)
+ {
+ this.fadeNearOrigin = flag;
+ return this;
+ }
+
+ public Builder shadedClouds(boolean flag)
+ {
+ this.shadedClouds = flag;
+ return this;
+ }
+
+ public Builder meshGenInterval(int interval)
+ {
+ if (interval <= 0)
+ throw new IllegalArgumentException("Mesh gen interval must be greater than 0");
+ this.meshGenIntervalCalculator = () -> interval;
+ return this;
+ }
+
+ public Builder meshGenInterval(Supplier calculator)
+ {
+ this.meshGenIntervalCalculator = calculator;
+ return this;
+ }
+
+ public Builder lodConfig(LevelOfDetailConfig config)
+ {
+ this.lodConfig = config;
+ return this;
+ }
+
+ public Builder useTransparency(boolean flag)
+ {
+ this.useTransparency = flag;
+ return this;
+ }
+
+ public Builder fixedMeshDataSectionSize(boolean flag)
+ {
+ this.fixedMeshDataSectionSize = flag;
+ return this;
+ }
+
+ public Builder fadeStart(float fadeStart)
+ {
+ this.fadeStart = fadeStart;
+ return this;
+ }
+
+ public Builder fadeEnd(float fadeEnd)
+ {
+ this.fadeEnd = fadeEnd;
+ return this;
+ }
+
+ public Builder testFacesFacingAway(boolean flag)
+ {
+ this.testFacesFacingAway = flag;
+ return this;
+ }
+
+ private T applyExtraSettings(T generator)
+ {
+ generator.setFadeDistances(this.fadeStart, this.fadeEnd);
+ generator.setTestFacesFacingAway(this.testFacesFacingAway);
+ return generator;
+ }
+
+ public MultiRegionCloudMeshGenerator createMultiRegion()
+ {
+ return this.applyExtraSettings(new MultiRegionCloudMeshGenerator(this.fadeNearOrigin, this.shadedClouds, this.lodConfig, this.meshGenIntervalCalculator, this.useTransparency, this.fixedMeshDataSectionSize));
+ }
+
+ public SingleRegionCloudMeshGenerator createSingleRegion(CloudInfo type)
+ {
+ return this.applyExtraSettings(new SingleRegionCloudMeshGenerator(this.shadedClouds, this.lodConfig, this.meshGenIntervalCalculator, this.useTransparency, this.fixedMeshDataSectionSize, type));
+ }
+ }
+}
diff --git a/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/generator/MultiRegionCloudMeshGenerator.java b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/generator/MultiRegionCloudMeshGenerator.java
new file mode 100644
index 00000000..8226742f
--- /dev/null
+++ b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/generator/MultiRegionCloudMeshGenerator.java
@@ -0,0 +1,404 @@
+package dev.nonamecrackers2.simpleclouds.client.mesh.generator;
+
+import java.io.IOException;
+import java.nio.IntBuffer;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.joml.Matrix2f;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL12;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL41;
+import org.lwjgl.opengl.GL42;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.mojang.blaze3d.platform.TextureUtil;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import dev.nonamecrackers2.simpleclouds.SimpleCloudsMod;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetail;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetailConfig;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.PreparedChunk;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.BindingManager;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.ShaderStorageBufferObject;
+import dev.nonamecrackers2.simpleclouds.client.shader.compute.ComputeShader;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudInfo;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudGetter;
+import dev.nonamecrackers2.simpleclouds.common.noise.AbstractNoiseSettings;
+import dev.nonamecrackers2.simpleclouds.common.noise.NoiseSettings;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+
+public final class MultiRegionCloudMeshGenerator extends CloudMeshGenerator
+{
+ private static final Logger LOGGER = LogManager.getLogger("simpleclouds/MultiRegionCloudMeshGenerator");
+
+ private static final ResourceLocation REGION_GENERATOR_LOC = SimpleCloudsMod.id("cloud_regions");
+ private static final String LOD_SCALES_NAME = "LodScales";
+ private static final String CLOUD_REGIONS_NAME = "CloudRegions";
+ public static final int MAX_CLOUD_TYPES = 64;
+ public static final int MAX_CLOUD_FORMATIONS = 10;
+ private static final int BYTES_PER_REGION = 32;
+ private int requiredRegionTexSize;
+ private CloudGetter cloudGetter = CloudGetter.EMPTY;
+ private CloudInfo[] cachedTypes = new CloudInfo[0];
+ private @Nullable ComputeShader regionTextureGenerator;
+ private int cloudRegionTextureId = -1;
+ private int cloudRegionImageBinding = -1;
+ private boolean updateCloudTypes;
+ private int currentCloudFormationCount;
+
+ protected MultiRegionCloudMeshGenerator(boolean fadeNearOrigin, boolean shadedClouds, LevelOfDetailConfig lodConfig, Supplier meshGenIntervalCalculator, boolean useTransparency, boolean fixedMeshDataSectionSize)
+ {
+ super(CloudMeshGenerator.MAIN_CUBE_MESH_GENERATOR, 0, fadeNearOrigin, shadedClouds, lodConfig, meshGenIntervalCalculator, useTransparency, fixedMeshDataSectionSize);
+ }
+
+ public void setCloudGetter(CloudGetter getter)
+ {
+ this.cloudGetter = Objects.requireNonNull(getter, "Cloud getter cannot be null");
+ this.updateCloudTypes();
+ }
+
+ public int getCloudRegionTextureId()
+ {
+ return this.cloudRegionTextureId;
+ }
+
+ public void updateCloudTypes()
+ {
+ this.updateCloudTypes = true;
+ }
+
+ public int getTotalCloudTypes()
+ {
+ return this.cachedTypes.length;
+ }
+
+ public int getCloudFormationCount()
+ {
+ return this.currentCloudFormationCount;
+ }
+
+ @Override
+ protected void setupShader()
+ {
+ super.setupShader();
+
+ this.cachedTypes = new CloudInfo[0];
+ this.updateCloudTypes = false;
+
+ this.shader.createAndBindSSBO(NOISE_LAYERS_NAME, GL15.GL_STATIC_DRAW).allocateBuffer(AbstractNoiseSettings.Param.values().length * 4 * MAX_NOISE_LAYERS * MAX_CLOUD_TYPES);
+ this.shader.createAndBindSSBO(LAYER_GROUPINGS_NAME, GL15.GL_STATIC_DRAW).allocateBuffer(CloudInfo.BYTES_PER_TYPE * MAX_CLOUD_TYPES);
+
+ this.uploadCloudTypeData();
+ }
+
+ @Override
+ protected void initExtra(ResourceManager manager) throws IOException
+ {
+ // Cloud region texture generator compute shader
+ // This texture is a 2D array texture, with a texture for each level of detail.
+ // The red channel contains the index for a cloud type in the main mesh compute shader, and
+ // the green channel contains an "edge fade" value for smooth cloud region boundaries.
+ // When generating the cloud mesh, the main mesh compute shader samples this array texture
+ // depending on what LOD it is generating for to determine what cloud type to construct
+
+ // Create the compute shader
+
+ this.currentCloudFormationCount = 0;
+ this.requiredRegionTexSize = 0;
+
+ if (this.regionTextureGenerator != null)
+ this.regionTextureGenerator.close();
+
+ var params = ImmutableMap.of("EDGE_FADE_FACTOR", String.valueOf(SimpleCloudsConstants.REGION_EDGE_FADE_FACTOR));
+ this.regionTextureGenerator = ComputeShader.loadShader(REGION_GENERATOR_LOC, manager, 16, 16, this.lodConfig.getLods().length + 1, params);
+
+ ShaderStorageBufferObject lodScales = this.regionTextureGenerator.createAndBindSSBO(LOD_SCALES_NAME, GL15.GL_STATIC_READ);
+ int lodScalesSize = this.lodConfig.getLods().length * 4 + 4;
+ lodScales.allocateBuffer(lodScalesSize);
+ lodScales.writeData(b -> {
+ b.putFloat(1.0F); // Primary chunk scale
+ for (LevelOfDetail l : this.lodConfig.getLods())
+ b.putFloat((float)l.chunkScale());
+ b.rewind();
+ }, lodScalesSize, false);
+
+ // Data for the cloud regions in world
+ this.regionTextureGenerator.createAndBindSSBO(CLOUD_REGIONS_NAME, GL15.GL_STATIC_READ).allocateBuffer(MAX_CLOUD_FORMATIONS * BYTES_PER_REGION);
+
+ // Create the cloud region 2D array texture
+
+ // Here we calculate the maximum size we need for this array texture,
+ // ensuring each block in the mesh will have a value to read in this texture
+ // when doing mesh generation
+ int prevSpan = this.lodConfig.getPrimaryChunkSpan();
+ int prevScale = 1;
+ int largestSpan = prevSpan;
+ for (LevelOfDetail config : this.lodConfig.getLods())
+ {
+ int scale = config.chunkScale();
+ int div = scale / prevScale;
+ prevScale = scale;
+ prevSpan = prevSpan / div + config.spread() * 2;
+ if (prevSpan > largestSpan)
+ largestSpan = prevSpan;
+ }
+ this.requiredRegionTexSize = largestSpan * SimpleCloudsConstants.CHUNK_SIZE;
+
+ if (this.cloudRegionTextureId != -1)
+ {
+ TextureUtil.releaseTextureId(this.cloudRegionTextureId);
+ this.cloudRegionTextureId = -1;
+ }
+
+ this.cloudRegionTextureId = TextureUtil.generateTextureId();
+ GL11.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.cloudRegionTextureId);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+ GL11.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+ GL12.glTexImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, GL30.GL_RG32F, this.requiredRegionTexSize, this.requiredRegionTexSize, this.lodConfig.getLods().length + 1, 0, GL30.GL_RG, GL11.GL_FLOAT, (IntBuffer)null);
+ GL11.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, 0);
+
+ // Assign an image unit to it so any shader can access it
+ if (this.cloudRegionImageBinding != -1)
+ BindingManager.freeImageUnit(this.cloudRegionImageBinding);
+ this.cloudRegionImageBinding = BindingManager.getAvailableImageUnit();
+ BindingManager.useImageUnit(this.cloudRegionImageBinding);
+ GL42.glBindImageTexture(this.cloudRegionImageBinding, this.cloudRegionTextureId, 0, true, 0, GL15.GL_WRITE_ONLY, GL30.GL_RG32F);
+ this.regionTextureGenerator.setImageUnit("regionTexture", this.cloudRegionImageBinding);
+
+ this.runRegionGenerator(0.0F, 0.0F, 1.0F);
+
+ // Update the main mesh shader to use this texture
+ this.shader.setSampler2DArray("RegionsSampler", this.cloudRegionTextureId, 0);
+ this.shader.forUniform("RegionsTexSize", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, this.requiredRegionTexSize);
+ });
+
+ LOGGER.debug("Created cloud region texture generator with size {}x{}x{}", this.requiredRegionTexSize, this.requiredRegionTexSize, this.lodConfig.getLods().length + 1);
+ }
+
+ @Override
+ protected CloudMeshGenerator.ChunkGenSettings determineChunkGenSettings(float minX, float minZ, float maxX, float maxZ)
+ {
+ float[][] positions = new float[][] { {minX, minZ}, {minX, maxZ}, {maxX, minZ}, {maxX, maxZ} };
+ int smallestStartHeight = 0;
+ int largestEndHeight = 0;
+ boolean empty = true;
+ for (int i = 0; i < positions.length; i++)
+ {
+ float[] pos = positions[i];
+ Pair typeAt = this.cloudGetter.getCloudTypeAtPosition(pos[0], pos[1]);
+ if (typeAt.getRight() < 1.0F)
+ empty = false;
+ NoiseSettings config = typeAt.getLeft().noiseConfig();
+ int startHeight = config.getStartHeight();
+ int endHeight = config.getEndHeight();
+ if (i == 0 || smallestStartHeight > startHeight)
+ smallestStartHeight = startHeight;
+ if (i == 0 || largestEndHeight < endHeight)
+ largestEndHeight = endHeight;
+ }
+ if (empty || smallestStartHeight == largestEndHeight)
+ return skip();
+ else
+ return heights(smallestStartHeight, largestEndHeight);
+ }
+
+ @Override
+ protected void generateChunk(CloudMeshGenerator.ChunkGenTask task)
+ {
+ this.shader.forUniform("RegionSampleOffset", (id, loc) ->
+ {
+ PreparedChunk chunk = task.chunk().getChunkInfo();
+ GL41.glProgramUniform2f(id, loc, chunk.x() * (float)SimpleCloudsConstants.CHUNK_SIZE + (float)this.requiredRegionTexSize / 2.0F, chunk.z() * (float)SimpleCloudsConstants.CHUNK_SIZE + (float)this.requiredRegionTexSize / 2.0F);
+ });
+ this.shader.setSampler2DArray("RegionsSampler", this.cloudRegionTextureId, 0);
+
+ super.generateChunk(task);
+ }
+
+ private void runRegionGenerator(float meshOffsetX, float meshOffsetZ, float partialTick)
+ {
+ if (this.regionTextureGenerator == null || !this.regionTextureGenerator.isValid())
+ return;
+
+ this.uploadCloudRegionData(partialTick);
+ this.regionTextureGenerator.forUniform("Offset", (id, loc) -> {
+ GL41.glProgramUniform2f(id, loc, meshOffsetX, meshOffsetZ);
+ });
+ this.regionTextureGenerator.dispatchAndWait(this.requiredRegionTexSize / 16, this.requiredRegionTexSize / 16, 1);
+ }
+
+ private void uploadCloudRegionData(float partialTick)
+ {
+ if (this.regionTextureGenerator == null || !this.regionTextureGenerator.isValid())
+ return;
+
+ // Converts the cloud regions into data we can then easily pack into
+ // the SSBO. This method also checks and excludes cloud regions that
+ // reference cloud types that are not set up in this mesh generator
+ // to avoid errors.
+ List regionData = this.cloudGetter.getClouds().stream().map(region ->
+ {
+ Matrix2f transform = region.createTransform(partialTick);
+ float[] data = new float[] {
+ region.getPosX(partialTick),
+ region.getPosZ(partialTick),
+ (float)ArrayUtils.indexOf(this.cachedTypes, this.cloudGetter.getCloudTypeForId(region.getCloudTypeId())),
+ region.getRadius(partialTick),
+ transform.m00,
+ transform.m01,
+ transform.m10,
+ transform.m11
+ };
+ return data;
+ }).filter(data -> data[2] >= 0.0F).toList();
+
+ int regionDataSize = regionData.size();
+ int count = Math.min(MAX_CLOUD_FORMATIONS, regionDataSize);
+ if (regionDataSize != this.currentCloudFormationCount)
+ {
+ if (regionDataSize > MAX_CLOUD_FORMATIONS && regionDataSize > this.currentCloudFormationCount)
+ LOGGER.warn("Cloud formations {}/{}. Maximum count has been exceeded; some cloud formations will be ignored. Please ensure cloud formation count does not exceed the maximum of {}.", regionData.size(), MAX_CLOUD_FORMATIONS, MAX_CLOUD_FORMATIONS);
+ this.currentCloudFormationCount = regionDataSize;
+ }
+
+ if (count > 0)
+ {
+ ShaderStorageBufferObject regionsBuffer = this.regionTextureGenerator.getShaderStorageBuffer("CloudRegions");
+ regionsBuffer.writeData(b ->
+ {
+ for (int i = 0; i < count; i++)
+ {
+ float[] data = regionData.get(i);
+ for (float f : data)
+ b.putFloat(f);
+ }
+ b.rewind();
+ }, count * BYTES_PER_REGION, false);
+ }
+
+ this.regionTextureGenerator.forUniform("TotalCloudRegions", (id, loc) -> {
+ GL41.glProgramUniform1i(id, loc, count);
+ });
+ }
+
+ private void uploadCloudTypeData()
+ {
+ RenderSystem.assertOnRenderThreadOrInit();
+
+ if (this.shader != null && this.shader.isValid())
+ {
+ var toCopy = this.cloudGetter.getIndexedCloudTypes();
+ if (toCopy.length > MAX_CLOUD_TYPES)
+ LOGGER.warn("Cloud type count exceeds the maximum. Not all cloud types will render.");
+ int copySize = Math.min(MAX_CLOUD_TYPES, toCopy.length);
+ this.cachedTypes = Arrays.copyOf(toCopy, copySize);
+
+ LOGGER.debug("Uploading cloud type noise data...");
+
+ this.shader.getShaderStorageBuffer(LAYER_GROUPINGS_NAME).writeData(b ->
+ {
+ int previousLayerIndex = 0;
+ for (int i = 0; i < this.cachedTypes.length; i++)
+ {
+ CloudInfo type = this.cachedTypes[i];
+ previousLayerIndex = type.packToBuffer(b, previousLayerIndex);
+ }
+ b.rewind();
+ }, CloudInfo.BYTES_PER_TYPE * this.cachedTypes.length, false);
+
+ this.shader.getShaderStorageBuffer(NOISE_LAYERS_NAME).writeData(b ->
+ {
+ for (int i = 0; i < this.cachedTypes.length; i++)
+ {
+ NoiseSettings settings = this.cachedTypes[i].noiseConfig();
+ float[] packed = settings.packForShader();
+ for (int j = 0; j < packed.length && j < AbstractNoiseSettings.Param.values().length * MAX_NOISE_LAYERS; j++)
+ b.putFloat(packed[j]);
+ }
+ b.rewind();
+ }, AbstractNoiseSettings.Param.values().length * 4 * MAX_NOISE_LAYERS * this.cachedTypes.length, false);
+ }
+ }
+
+ @Override
+ protected int prepareMeshGen(double originX, double originY, double originZ, float meshGenOffsetX, float meshGenOffsetZ, @Nullable Frustum frustum, int interval, float partialTick)
+ {
+ if (this.updateCloudTypes)
+ {
+ this.uploadCloudTypeData();
+ this.updateCloudTypes = false;
+ }
+
+ this.runRegionGenerator(meshGenOffsetX, meshGenOffsetZ, partialTick);
+
+ return super.prepareMeshGen(originX, originY, originZ, meshGenOffsetX, meshGenOffsetZ, frustum, interval, partialTick);
+ }
+
+ @Override
+ protected void onOffGen()
+ {
+ super.onOffGen();
+
+ if (this.regionTextureGenerator != null)
+ this.regionTextureGenerator.getShaderStorageBuffer("CloudRegions").readData(buf -> {}, BYTES_PER_REGION * MAX_CLOUD_FORMATIONS);
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+
+ this.currentCloudFormationCount = 0;
+ this.requiredRegionTexSize = 0;
+ this.updateCloudTypes = false;
+ this.cloudGetter = CloudGetter.EMPTY;
+ this.cachedTypes = new CloudInfo[0];
+
+ if (this.regionTextureGenerator != null)
+ {
+ this.regionTextureGenerator.close();
+ this.regionTextureGenerator = null;
+ }
+
+ if (this.cloudRegionTextureId != -1)
+ {
+ TextureUtil.releaseTextureId(this.cloudRegionTextureId);
+ this.cloudRegionTextureId = -1;
+ }
+
+ if (this.cloudRegionImageBinding != -1)
+ {
+ BindingManager.freeImageUnit(this.cloudRegionImageBinding);
+ this.cloudRegionImageBinding = -1;
+ }
+ }
+
+ @Override
+ public void fillReport(CrashReportCategory category)
+ {
+ category.setDetail("Cloud Types", "(" + this.cachedTypes.length + ") " + Joiner.on(", ").join(this.cachedTypes));
+ category.setDetail("Cloud Regions", this.cloudGetter.getClouds().size());
+ category.setDetail("Cloud Formations", this.currentCloudFormationCount);
+ super.fillReport(category);
+ }
+}
diff --git a/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/instancing/InstanceableMesh.java b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/instancing/InstanceableMesh.java
new file mode 100644
index 00000000..49a38c40
--- /dev/null
+++ b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/mesh/instancing/InstanceableMesh.java
@@ -0,0 +1,219 @@
+package dev.nonamecrackers2.simpleclouds.client.mesh.instancing;
+
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL31;
+import org.lwjgl.system.MemoryUtil;
+
+import com.mojang.blaze3d.platform.MemoryTracker;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import com.mojang.blaze3d.vertex.VertexFormat;
+
+public class InstanceableMesh
+{
+ private int arrayObjectId = -1;
+ private int vertexBufferId = -1;
+ private int indexBufferId = -1;
+ private @Nullable ByteBuffer vertexBuffer;
+ private @Nullable ByteBuffer indexBuffer;
+ private int totalIndices;
+
+ public InstanceableMesh(int vertexBufferSize, int indexBufferSize, VertexFormat format, Consumer vertexBufferGenerator, Function indexBufferGenerator)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ this.arrayObjectId = GL30.glGenVertexArrays();
+ this.vertexBufferId = GL15.glGenBuffers();
+ this.indexBufferId = GL15.glGenBuffers();
+
+ GL30.glBindVertexArray(this.arrayObjectId);
+
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, this.vertexBufferId);
+ this.vertexBuffer = MemoryTracker.create(vertexBufferSize);
+ vertexBufferGenerator.accept(this.vertexBuffer);
+ GL15.glBufferData(GL15.GL_ARRAY_BUFFER, this.vertexBuffer, GL15.GL_STATIC_DRAW);
+ format.setupBufferState();
+ GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, this.indexBufferId);
+ this.indexBuffer = MemoryTracker.create(indexBufferSize);
+ this.totalIndices = indexBufferGenerator.apply(this.indexBuffer);
+ GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer, GL15.GL_STATIC_DRAW);
+
+ GL30.glBindVertexArray(0);
+ }
+
+ public static InstanceableMesh defaultSide()
+ {
+ return new InstanceableMesh(48, 24, DefaultVertexFormat.POSITION, buffer ->
+ {
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.rewind();
+ }, buffer ->
+ {
+ buffer.putInt(0);
+ buffer.putInt(1);
+ buffer.putInt(2);
+ buffer.putInt(0);
+ buffer.putInt(2);
+ buffer.putInt(3);
+ buffer.rewind();
+ return 6;
+ });
+ }
+
+ public static InstanceableMesh defaultNonCulledSide()
+ {
+ return new InstanceableMesh(48, 48, DefaultVertexFormat.POSITION, buffer ->
+ {
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.rewind();
+ }, buffer ->
+ {
+ buffer.putInt(0);
+ buffer.putInt(1);
+ buffer.putInt(2);
+ buffer.putInt(0);
+ buffer.putInt(2);
+ buffer.putInt(3);
+
+ buffer.putInt(2);
+ buffer.putInt(1);
+ buffer.putInt(0);
+ buffer.putInt(3);
+ buffer.putInt(2);
+ buffer.putInt(0);
+
+ buffer.rewind();
+ return 12;
+ });
+ }
+
+// public static PreparedMesh defaultCube()
+// {
+// return new PreparedMesh(576, 144, SimpleCloudsShaders.POSITION_NORMAL, buffer -> {
+// //-x
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// //+x
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)0);
+// //-y
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0); buffer.put((byte)0);
+// //+y
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0); buffer.put((byte)0);
+// //-z
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)-1); buffer.put((byte)0);
+// //-z
+// buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+// buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+// buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.put((byte)0); buffer.put((byte)0); buffer.put((byte)1); buffer.put((byte)0);
+//
+// buffer.rewind();
+// }, buffer -> {
+// buffer.putInt(0); buffer.putInt(1); buffer.putInt(2); buffer.putInt(0); buffer.putInt(2); buffer.putInt(3); // -x
+// buffer.putInt(4); buffer.putInt(5); buffer.putInt(6); buffer.putInt(4); buffer.putInt(6); buffer.putInt(7); // +x
+// buffer.putInt(8); buffer.putInt(9); buffer.putInt(10); buffer.putInt(8); buffer.putInt(10); buffer.putInt(11); // -y
+// buffer.putInt(12); buffer.putInt(13); buffer.putInt(14); buffer.putInt(12); buffer.putInt(14); buffer.putInt(15); // +y
+// buffer.putInt(16); buffer.putInt(17); buffer.putInt(18); buffer.putInt(16); buffer.putInt(18); buffer.putInt(19); // -z
+// buffer.putInt(20); buffer.putInt(21); buffer.putInt(22); buffer.putInt(20); buffer.putInt(22); buffer.putInt(23); // +z
+// buffer.rewind();
+// return 36;
+// });
+// }
+
+ public static InstanceableMesh defaultCube()
+ {
+ return new InstanceableMesh(96, 144, DefaultVertexFormat.POSITION, buffer ->
+ {
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat(-1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat( 1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat( 1.0F); buffer.putFloat( 1.0F);
+ buffer.putFloat(-1.0F); buffer.putFloat(-1.0F); buffer.putFloat( 1.0F);
+ buffer.rewind();
+ }, buffer ->
+ {
+ buffer.putInt(0); buffer.putInt(1); buffer.putInt(2); buffer.putInt(0); buffer.putInt(2); buffer.putInt(3); // -z
+ buffer.putInt(4); buffer.putInt(7); buffer.putInt(6); buffer.putInt(4); buffer.putInt(6); buffer.putInt(5); // +z
+ buffer.putInt(7); buffer.putInt(0); buffer.putInt(3); buffer.putInt(7); buffer.putInt(3); buffer.putInt(6); // -x
+ buffer.putInt(1); buffer.putInt(4); buffer.putInt(5); buffer.putInt(1); buffer.putInt(5); buffer.putInt(2); // +x
+ buffer.putInt(1); buffer.putInt(0); buffer.putInt(7); buffer.putInt(1); buffer.putInt(7); buffer.putInt(4); // -y
+ buffer.putInt(5); buffer.putInt(6); buffer.putInt(3); buffer.putInt(5); buffer.putInt(3); buffer.putInt(2); // +y
+ buffer.rewind();
+ return 36;
+ });
+ }
+
+ public void drawInstanced(int count)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ GL30.glBindVertexArray(this.arrayObjectId);
+ GL31.glDrawElementsInstanced(GL11.GL_TRIANGLES, this.totalIndices, GL11.GL_UNSIGNED_INT, 0L, count);
+ }
+
+ public void destroy()
+ {
+ this.totalIndices = 0;
+
+ if (this.arrayObjectId >= 0)
+ {
+ RenderSystem.glDeleteVertexArrays(this.arrayObjectId);
+ this.arrayObjectId = -1;
+ }
+
+ if (this.vertexBufferId >= 0)
+ {
+ RenderSystem.glDeleteBuffers(this.vertexBufferId);
+ this.vertexBufferId = -1;
+ }
+
+ if (this.vertexBuffer != null)
+ {
+ MemoryUtil.memFree(this.vertexBuffer);
+ this.vertexBuffer = null;
+ }
+
+ if (this.indexBufferId >= 0)
+ {
+ RenderSystem.glDeleteBuffers(this.indexBufferId);
+ this.indexBufferId = -1;
+ }
+
+ if (this.indexBuffer != null)
+ {
+ MemoryUtil.memFree(this.indexBuffer);
+ this.indexBuffer = null;
+ }
+ }
+}
diff --git a/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer.java b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer.java
new file mode 100644
index 00000000..7bd37397
--- /dev/null
+++ b/tmp_simpleclouds_src/dev/nonamecrackers2/simpleclouds/client/renderer/SimpleCloudsRenderer.java
@@ -0,0 +1,1497 @@
+package dev.nonamecrackers2.simpleclouds.client.renderer;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+import org.joml.Matrix4f;
+import org.joml.Vector2f;
+import org.joml.Vector3f;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL14;
+import org.lwjgl.opengl.GL15;
+import org.lwjgl.opengl.GL30;
+import org.lwjgl.opengl.GL40;
+import org.lwjgl.opengl.GL43;
+
+import com.google.common.collect.Lists;
+import com.google.gson.JsonSyntaxException;
+import com.mojang.blaze3d.pipeline.RenderTarget;
+import com.mojang.blaze3d.pipeline.TextureTarget;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.platform.Window;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.BufferBuilder;
+import com.mojang.blaze3d.vertex.BufferUploader;
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.Tesselator;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+import com.mojang.blaze3d.vertex.VertexFormat;
+import com.mojang.math.Axis;
+
+import dev.nonamecrackers2.simpleclouds.SimpleCloudsMod;
+import dev.nonamecrackers2.simpleclouds.api.client.event.ModifyCloudRenderDistanceEvent;
+import dev.nonamecrackers2.simpleclouds.api.common.cloud.CloudMode;
+import dev.nonamecrackers2.simpleclouds.client.cloud.ClientSideCloudTypeManager;
+import dev.nonamecrackers2.simpleclouds.client.compat.SimpleCloudsCompatHelper;
+import dev.nonamecrackers2.simpleclouds.client.event.impl.DetermineCloudRenderPipelineEvent;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.CloudRenderTarget;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.ShadowMapBuffer;
+import dev.nonamecrackers2.simpleclouds.client.framebuffer.WeightedBlendingTarget;
+import dev.nonamecrackers2.simpleclouds.client.mesh.RendererInitializeResult;
+import dev.nonamecrackers2.simpleclouds.client.mesh.chunk.MeshChunk;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.CloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.MultiRegionCloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.mesh.generator.SingleRegionCloudMeshGenerator;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.LevelOfDetailConfig;
+import dev.nonamecrackers2.simpleclouds.client.mesh.lod.PreparedChunk;
+import dev.nonamecrackers2.simpleclouds.client.renderer.lightning.LightningBolt;
+import dev.nonamecrackers2.simpleclouds.client.renderer.pipeline.CloudsRenderPipeline;
+import dev.nonamecrackers2.simpleclouds.client.renderer.settings.CloudsRendererSettings;
+import dev.nonamecrackers2.simpleclouds.client.shader.SimpleCloudsShaders;
+import dev.nonamecrackers2.simpleclouds.client.shader.SingleSSBOShaderInstance;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.BindingManager;
+import dev.nonamecrackers2.simpleclouds.client.shader.buffer.ShaderStorageBufferObject;
+import dev.nonamecrackers2.simpleclouds.client.world.ClientCloudManager;
+import dev.nonamecrackers2.simpleclouds.common.cloud.CloudType;
+import dev.nonamecrackers2.simpleclouds.common.cloud.SimpleCloudsConstants;
+import dev.nonamecrackers2.simpleclouds.common.cloud.region.CloudGetter;
+import dev.nonamecrackers2.simpleclouds.common.config.SimpleCloudsConfig;
+import dev.nonamecrackers2.simpleclouds.mixin.MixinPostChain;
+import net.minecraft.CrashReport;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.renderer.EffectInstance;
+import net.minecraft.client.renderer.GameRenderer;
+import net.minecraft.client.renderer.LevelRenderer;
+import net.minecraft.client.renderer.LightTexture;
+import net.minecraft.client.renderer.PostChain;
+import net.minecraft.client.renderer.PostPass;
+import net.minecraft.client.renderer.ShaderInstance;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.client.renderer.texture.AbstractTexture;
+import net.minecraft.client.renderer.texture.TextureManager;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
+import net.minecraft.util.FastColor;
+import net.minecraft.util.Mth;
+import net.minecraft.util.profiling.ProfilerFiller;
+import net.minecraft.world.effect.MobEffectInstance;
+import net.minecraft.world.effect.MobEffects;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.phys.Vec3;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.StartupMessageManager;
+import net.minecraftforge.fml.loading.ImmediateWindowHandler;
+import nonamecrackers2.crackerslib.common.compat.CompatHelper;
+
+public class SimpleCloudsRenderer implements ResourceManagerReloadListener
+{
+ private static final Logger LOGGER = LogManager.getLogger("simpleclouds/SimpleCloudsRenderer");
+ private static final Vector3f DIFFUSE_LIGHT_0 = (new Vector3f(0.2F, 1.0F, -0.7F)).normalize();
+ private static final Vector3f DIFFUSE_LIGHT_1 = (new Vector3f(-0.2F, 1.0F, 0.7F)).normalize();
+ private static final ResourceLocation STORM_POST_PROCESSING_LOC = SimpleCloudsMod.id("shaders/post/storm_post.json");
+ private static final ResourceLocation BLUR_POST_PROCESSING_LOC = SimpleCloudsMod.id("shaders/post/blur_post.json");
+ private static final ResourceLocation SCREEN_SPACE_WORLD_FOG_LOC = SimpleCloudsMod.id("shaders/post/screen_space_world_fog.json");
+ private static final ResourceLocation CLOUD_SHADOWS_LOC = SimpleCloudsMod.id("shaders/post/cloud_shadows.json");
+ public static final ResourceLocation FINAL_COMPOSITE_LOC = SimpleCloudsMod.id("shaders/post/final_composite.json");
+ public static final ResourceLocation FINAL_COMPOSITE_NO_TRANSPARENCY_LOC = SimpleCloudsMod.id("shaders/post/final_composite_no_transparency.json");
+ private static final ResourceLocation DITHER_TEXTURE = SimpleCloudsMod.id("textures/shader/bayer_matrix.png");
+ private static final ArtifactVersion REQUIRED_OPENGL_VERSION = new DefaultArtifactVersion("4.3");
+ public static final int SHADOW_MAP_SIZE = 1024;
+ public static final int SHADOW_MAP_SPAN = 10000;
+ public static final int MAX_LIGHTNING_BOLTS = 16;
+ public static final int BYTES_PER_LIGHTNING_BOLT = 16;
+ public static final float CHUNK_FADE_IN_ALPHA_PER_TICK = 0.2F;
+ public static final float DITHER_SCALE = 0.05F;
+ private static @Nullable SimpleCloudsRenderer instance;
+ private final CloudsRendererSettings settings;
+ private final Minecraft mc;
+ private final WorldEffects worldEffectsManager;
+ private final AtmosphericCloudsRenderHandler atmoshpericClouds;
+ private @Nullable ClientCloudManager cloudManager;
+ private ArtifactVersion openGlVersion;
+ private CloudMeshGenerator meshGenerator;
+ private @Nullable CloudsRenderPipeline renderPipelineThisPass;
+ private @Nullable RenderTarget cloudTarget;
+ private @Nullable WeightedBlendingTarget cloudTransparencyTarget;
+ private @Nullable RenderTarget stormFogTarget;
+ private int stormFogResolutionDivisor = 4;
+ private @Nullable RenderTarget blurTarget;
+ private final List postChains = Lists.newArrayList();
+ private @Nullable PostChain finalComposite;
+ private @Nullable PostChain stormPostProcessing;
+ private @Nullable PostChain blurPostProcessing;
+ private @Nullable PostChain screenSpaceWorldFog;
+ private @Nullable PostChain cloudShadows;
+ private @Nullable ShaderStorageBufferObject lightningBoltPositions;
+ private @Nullable ShadowMapBuffer stormFogShadowMap;
+ private Optional shadowMap = Optional.empty();
+ private @Nullable Frustum cullFrustum;
+ private float fogStart;
+ private float fogEnd;
+ private @Nullable PoseStack stormFogShadowMapStack;
+ private @Nullable PoseStack shadowMapStack;
+ private boolean failedToCopyDepthBuffer;
+ private boolean needsReload;
+ private @Nullable RendererInitializeResult initialInitializationResult;
+
+ private SimpleCloudsRenderer(CloudsRendererSettings settings, Minecraft mc)
+ {
+ this.settings = settings;
+ this.mc = mc;
+ this.worldEffectsManager = new WorldEffects(mc, this);
+ this.atmoshpericClouds = new AtmosphericCloudsRenderHandler(mc);
+ }
+
+ public String getClientCloudManagerString()
+ {
+ return this.cloudManager != null ? this.cloudManager.toString() : "null";
+ }
+
+ public CloudMeshGenerator getMeshGenerator()
+ {
+ return this.meshGenerator;
+ }
+
+ public CloudsRenderPipeline getRenderPipeline()
+ {
+ return Objects.requireNonNull(this.renderPipelineThisPass, "Pipeline not determined");
+ }
+
+ public WorldEffects getWorldEffectsManager()
+ {
+ return this.worldEffectsManager;
+ }
+
+ public AtmosphericCloudsRenderHandler getAtmosphericCloudRenderer()
+ {
+ return this.atmoshpericClouds;
+ }
+
+ public CloudsRendererSettings getSettings()
+ {
+ return this.settings;
+ }
+
+ public @Nullable RendererInitializeResult getInitialInitializationResult()
+ {
+ return this.initialInitializationResult;
+ }
+
+ public ShadowMapBuffer getStormFogShadowMap()
+ {
+ return this.stormFogShadowMap;
+ }
+
+ public Optional getShadowMap()
+ {
+ return this.shadowMap;
+ }
+
+ public @Nullable PoseStack getStormFogShadowMapStack()
+ {
+ return this.stormFogShadowMapStack;
+ }
+
+ public @Nullable PoseStack getShadowMapStack()
+ {
+ return this.shadowMapStack;
+ }
+
+ public RenderTarget getBlurTarget()
+ {
+ return this.blurTarget;
+ }
+
+ public RenderTarget getStormFogTarget()
+ {
+ return this.stormFogTarget;
+ }
+
+ public RenderTarget getCloudTarget()
+ {
+ return this.cloudTarget;
+ }
+
+ public WeightedBlendingTarget getCloudTransparencyTarget()
+ {
+ return this.cloudTransparencyTarget;
+ }
+
+ public float getFogStart()
+ {
+ return this.fogStart;
+ }
+
+ public float getFogEnd()
+ {
+ return this.fogEnd;
+ }
+
+ public float getFadeFactorForDistance(float distance)
+ {
+ return 1.0F - Math.min(Math.max(distance - this.fogStart, 0.0F) / (this.fogEnd - this.fogStart), 1.0F);
+ }
+
+ public @Nullable Frustum getCullFrustum()
+ {
+ return this.cullFrustum;
+ }
+
+ public void onCloudManagerChange(ClientCloudManager manager)
+ {
+ this.cloudManager = manager;
+ if (this.meshGenerator instanceof MultiRegionCloudMeshGenerator generator)
+ generator.setCloudGetter(manager);
+ }
+
+ private void prepareMeshGenerator(float partialTicks)
+ {
+ if (this.meshGenerator instanceof SingleRegionCloudMeshGenerator generator)
+ generator.setFadeDistances((float)SimpleCloudsConfig.CLIENT.singleModeFadeStartPercentage.get() / 100.0F, (float)SimpleCloudsConfig.CLIENT.singleModeFadeEndPercentage.get() / 100.0F);
+ this.meshGenerator.setTransparencyRenderDistance((float)SimpleCloudsConfig.CLIENT.transparencyRenderDistancePercentage.get() / 100.0F);
+ this.meshGenerator.setTestFacesFacingAway(SimpleCloudsConfig.CLIENT.testSidesThatAreOccluded.get());
+ if (this.mc.level != null)
+ {
+ this.meshGenerator.setScroll(this.cloudManager.getScrollX(partialTicks), this.cloudManager.getScrollY(partialTicks), this.cloudManager.getScrollZ(partialTicks));
+ }
+ }
+
+ public boolean needsReinitialization()
+ {
+ return this.settings.needsReinitialization(this.meshGenerator);
+ }
+
+ public void requestReload()
+ {
+ LOGGER.debug("Requesting reload...");
+ this.needsReload = true;
+ }
+
+ @Override
+ public void onResourceManagerReload(ResourceManager manager)
+ {
+ RenderSystem.assertOnRenderThreadOrInit();
+
+ this.initialInitializationResult = null;
+
+ // --- Check OpenGL version ---
+
+ ArtifactVersion openGlVersion = this.openGlVersion;
+ if (openGlVersion == null)
+ openGlVersion = new DefaultArtifactVersion(ImmediateWindowHandler.getGLVersion());
+ if (openGlVersion.compareTo(REQUIRED_OPENGL_VERSION) < 0)
+ {
+ LOGGER.error("Simple Clouds renderer could not initialize. OpenGL version is {}, minimum required is {}", openGlVersion, REQUIRED_OPENGL_VERSION);
+ this.initialInitializationResult = RendererInitializeResult.builder().errorOpenGL().build();
+ this.openGlVersion = openGlVersion;
+ return;
+ }
+
+ if (!SimpleCloudsShaders.areShadersInitialized())
+ {
+ LOGGER.error("Simple Clouds renderer could not initialize. Core shaders are not initialized.");
+ this.initialInitializationResult = RendererInitializeResult.builder().coreShadersNotInitialized(SimpleCloudsShaders.getError()).build();
+ saveAndPrintCrashReports(this.mc, this.initialInitializationResult);
+ return;
+ }
+
+ RendererInitializeResult compatError = SimpleCloudsCompatHelper.findCompatErrors();
+ if (compatError.getState() == RendererInitializeResult.State.ERROR)
+ {
+ LOGGER.error("Simple Clouds renderer could not initialize due to compat error(s): {}", compatError.getErrors().stream().map(e -> e.text().getString()).toList());
+ this.initialInitializationResult = compatError;
+ saveAndPrintCrashReports(this.mc, this.initialInitializationResult);
+ return;
+ }
+
+ StartupMessageManager.addModMessage("Initializing Simple Clouds renderer");
+
+ LOGGER.debug("OpenGL {}", openGlVersion);
+
+ Instant started = Instant.now();
+
+ LOGGER.debug("Beginning Simple Clouds renderer initialization");
+
+ this.failedToCopyDepthBuffer = false;
+
+ // --- Render Targets ---
+
+ boolean highPrecisionDepth = SimpleCloudsMod.dhLoaded();
+
+ RenderTarget main = SimpleCloudsCompatHelper.getMainRenderTarget();
+ if (main == null)
+ {
+ this.initialInitializationResult = RendererInitializeResult.builder().errorUnknown(new NullPointerException("Main framebuffer is null"), "Simple Clouds Renderer").build();
+ saveAndPrintCrashReports(this.mc, this.initialInitializationResult);
+ return;
+ }
+
+ if (this.cloudTarget != null)
+ this.cloudTarget.destroyBuffers();
+ this.cloudTarget = new CloudRenderTarget(main.width, main.height, Minecraft.ON_OSX, highPrecisionDepth);
+ this.cloudTarget.setClearColor(0.0F, 0.0F, 0.0F, 0.0F);
+
+ if (this.cloudTransparencyTarget != null)
+ this.cloudTransparencyTarget.destroyBuffers();
+ this.cloudTransparencyTarget = new WeightedBlendingTarget(main.width, main.height, Minecraft.ON_OSX, highPrecisionDepth);
+
+ this.stormFogResolutionDivisor = SimpleCloudsCompatHelper.getStormFogResolutionDivisor();
+ if (this.stormFogTarget != null)
+ this.stormFogTarget.destroyBuffers();
+ this.stormFogTarget = new TextureTarget(main.width / this.stormFogResolutionDivisor, main.height / this.stormFogResolutionDivisor, false, Minecraft.ON_OSX);
+ this.stormFogTarget.setClearColor(0.0F, 0.0F, 0.0F, 0.0F);
+ this.stormFogTarget.setFilterMode(GL11.GL_LINEAR);
+
+ if (this.blurTarget != null)
+ this.blurTarget.destroyBuffers();
+ this.blurTarget = new TextureTarget(main.width, main.height, false, Minecraft.ON_OSX);
+ this.blurTarget.setClearColor(0.0F, 0.0F, 0.0F, 0.0F);
+ this.blurTarget.setFilterMode(GL11.GL_LINEAR);
+
+ // --- Mesh Generator ---
+
+ this.setupMeshGenerator(); // Create/setup the generator
+ this.prepareMeshGenerator(0.0F); // Prepare it
+
+ RendererInitializeResult result = this.meshGenerator.init(manager); // Initialize
+ if (this.initialInitializationResult == null)
+ this.initialInitializationResult = result;
+
+ // --- Shadow Map ---
+
+ if (this.stormFogShadowMap != null)
+ {
+ this.stormFogShadowMap.close();
+ this.stormFogShadowMap = null;
+ }
+
+ this.shadowMap.ifPresent(buffer -> {
+ buffer.close();
+ });
+
+ int span = this.meshGenerator.getLodConfig().getEffectiveChunkSpan() * SimpleCloudsConstants.CHUNK_SIZE * SimpleCloudsConstants.CLOUD_SCALE;
+ this.stormFogShadowMap = new ShadowMapBuffer(span, span, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0.0F, 10000.0F, true, false);
+
+ if (SimpleCloudsConfig.CLIENT.distantShadows.get() && SimpleCloudsMod.dhLoaded())
+ {
+ int distantShadowSpan = SimpleCloudsConfig.CLIENT.shadowDistance.get() * 2;
+ distantShadowSpan = Math.min(distantShadowSpan, span);
+ this.shadowMap = Optional.of(new ShadowMapBuffer(distantShadowSpan, distantShadowSpan, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0.0F, 10000.0F, false, true));
+ }
+ else
+ {
+ this.shadowMap = Optional.empty();
+ }
+
+ // --- Post Processing Shaders ---
+
+ this.destroyPostChains();
+
+ if (this.lightningBoltPositions != null)
+ {
+ BindingManager.freeSSBO(this.lightningBoltPositions);
+ this.lightningBoltPositions = null;
+ }
+
+ this.lightningBoltPositions = BindingManager.createSSBO(GL15.GL_DYNAMIC_DRAW);
+ this.lightningBoltPositions.allocateBuffer(MAX_LIGHTNING_BOLTS * BYTES_PER_LIGHTNING_BOLT);
+
+ this.stormPostProcessing = this.createPostChain(manager, STORM_POST_PROCESSING_LOC, this.stormFogTarget, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("ShadowMap", () -> this.stormFogShadowMap.getDepthTexId());
+ effect.setSampler("ShadowMapColor", () -> this.stormFogShadowMap.getColorTexId());
+ effect.setSampler("DepthSampler", () -> this.cloudTarget.getDepthTextureId());
+ this.lightningBoltPositions.optionalBindToProgram("LightningBolts", effect.getId());
+ });
+
+ this.blurPostProcessing = this.createPostChain(manager, BLUR_POST_PROCESSING_LOC, this.blurTarget);
+ this.blurPostProcessing.getTempTarget("swap").setFilterMode(GL11.GL_LINEAR);
+
+ this.screenSpaceWorldFog = this.createPostChain(manager, SCREEN_SPACE_WORLD_FOG_LOC, main, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("StormFogSampler", () -> this.blurTarget.getColorTextureId());
+ effect.setSampler("CloudDepthSampler", () -> this.cloudTarget.getDepthTextureId());
+ });
+
+ this.finalComposite = this.createPostChain(manager, this.settings.useTransparency() ? FINAL_COMPOSITE_LOC : FINAL_COMPOSITE_NO_TRANSPARENCY_LOC, main, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ if (this.settings.useTransparency())
+ {
+ effect.setSampler("AccumTexture", () -> this.cloudTransparencyTarget.getColorTextureId());
+ effect.setSampler("RevealageTexture", () -> this.cloudTransparencyTarget.getRevealageTextureId());
+ }
+ effect.setSampler("CloudsTexture", () -> this.cloudTarget.getColorTextureId());
+ });
+
+ if (this.shadowMap.isPresent())
+ {
+ ShadowMapBuffer map = this.shadowMap.get();
+ this.cloudShadows = this.createPostChain(manager, CLOUD_SHADOWS_LOC, main, pass ->
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("ShadowMap", () -> this.shadowMap.get().getDepthTexId());
+ effect.safeGetUniform("ShadowSpan").set((float)Math.min(map.getViewWidth(), map.getViewHeight()));
+ });
+ }
+
+ this.atmoshpericClouds.init(manager);
+
+ // --- Final debug ---
+
+ long duration = Duration.between(started, Instant.now()).toMillis();
+ LOGGER.info("Finished initialization, took {} ms", duration);
+
+ LOGGER.debug("Total LODs: {}", this.meshGenerator.getLodConfig().getLods().length + 1);
+ LOGGER.debug("Highest detail (primary) chunk span: {}", this.meshGenerator.getLodConfig().getPrimaryChunkSpan());
+ LOGGER.debug("Effective chunk span with LODs (total viewable area): {}", this.meshGenerator.getLodConfig().getEffectiveChunkSpan());
+ LOGGER.debug("Total span in blocks: {}", this.meshGenerator.getLodConfig().getEffectiveChunkSpan() * SimpleCloudsConstants.CHUNK_SIZE * SimpleCloudsConstants.CLOUD_SCALE);
+
+ //Print crash reports if needed
+ saveAndPrintCrashReports(this.mc, result);
+ }
+
+ private static void saveAndPrintCrashReports(Minecraft mc, RendererInitializeResult result)
+ {
+ switch (result.getState())
+ {
+ case ERROR:
+ {
+ List reports = result.createCrashReports();
+ LOGGER.error("---------CRASH REPORT BEGIN---------");
+ for (CrashReport report : reports)
+ {
+ mc.fillReport(report);
+ LOGGER.error("{}", report.getFriendlyReport());
+ }
+ LOGGER.error("---------CRASH REPORT END---------");
+ result.saveCrashReports(mc.gameDirectory);
+ break;
+ }
+ default:
+ }
+ }
+
+ private void setupMeshGenerator()
+ {
+ if (this.settings.checkAndOrBeginInitialization(this.meshGenerator))
+ {
+ if (this.meshGenerator != null)
+ {
+ this.meshGenerator.close(); //Close the current generator
+ this.meshGenerator = null;
+ }
+
+ CloudMode mode = this.settings.getCurrentCloudMode();
+ boolean isAmbientMode = mode == CloudMode.AMBIENT;
+ boolean useMultiRegion = isAmbientMode || mode == CloudMode.DEFAULT;
+ boolean shadedClouds = this.settings.shadedClouds();
+ boolean useFixedMeshDataSectionSize = this.settings.useFixedMeshDataSectionSize();
+ boolean useTransparency = this.settings.useTransparency();
+ LevelOfDetailConfig lod = this.settings.getCurrentLod().getConfig();
+
+ var builder = CloudMeshGenerator.builder()
+ .fadeNearOrigin(isAmbientMode)
+ .shadedClouds(shadedClouds)
+ .fixedMeshDataSectionSize(useFixedMeshDataSectionSize)
+ .meshGenInterval(SimpleCloudsRenderer::calculateMeshGenInterval)
+ .lodConfig(lod)
+ .useTransparency(useTransparency);
+
+ if (useMultiRegion) //Use the multi-region generator for DEFAULT or AMBIENT cloud mode
+ {
+ if (isAmbientMode)
+ {
+ builder.fadeStart(SimpleCloudsConstants.AMBIENT_MODE_FADE_START)
+ .fadeEnd(SimpleCloudsConstants.AMBIENT_MODE_FADE_END);
+ }
+ this.meshGenerator = builder.createMultiRegion();
+ }
+ else if (mode == CloudMode.SINGLE)
+ {
+ float fadeStart = (float)SimpleCloudsConfig.CLIENT.singleModeFadeStartPercentage.get() / 100.0F;
+ float fadeEnd = (float)SimpleCloudsConfig.CLIENT.singleModeFadeEndPercentage.get() / 100.0F;
+ this.meshGenerator = builder.fadeStart(fadeStart).fadeEnd(fadeEnd).createSingleRegion(SimpleCloudsConstants.EMPTY);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Not sure how to handle cloud mode " + mode);
+ }
+ }
+
+ if (this.meshGenerator instanceof MultiRegionCloudMeshGenerator multiRegionGenerator)
+ {
+ multiRegionGenerator.setCloudGetter(this.cloudManager != null ? this.cloudManager : CloudGetter.EMPTY);
+ }
+ else if (this.meshGenerator instanceof SingleRegionCloudMeshGenerator singleRegionGenerator)
+ {
+ //Find the desired single mode cloud type, either from the client-side only context or
+ //from the synced cloud types from the server
+ CloudType type = this.settings.getSingleModeCloudType();
+ if (!ClientCloudManager.isAvailableServerSide() && !ClientSideCloudTypeManager.isValidClientSideSingleModeCloudType(type))
+ type = SimpleCloudsConstants.EMPTY;
+ if (type == null)
+ type = SimpleCloudsConstants.EMPTY;
+ singleRegionGenerator.setCloudType(type);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Not sure how to handle generator: " + this.meshGenerator);
+ }
+ }
+
+ private void destroyPostChains()
+ {
+ this.postChains.forEach(PostChain::close);
+ this.postChains.clear();
+ }
+
+ private @Nullable PostChain createPostChain(ResourceManager manager, ResourceLocation loc, RenderTarget target)
+ {
+ return this.createPostChain(manager, loc, target, effect -> {});
+ }
+
+ private @Nullable PostChain createPostChain(ResourceManager manager, ResourceLocation loc, RenderTarget target, Consumer passConsumer)
+ {
+ try
+ {
+ PostChain chain = new PostChain(this.mc.getTextureManager(), manager, target, loc);
+ chain.resize(target.width, target.height);
+ for (PostPass pass : ((MixinPostChain)chain).simpleclouds$getPostPasses())
+ passConsumer.accept(pass);
+ this.postChains.add(chain);
+ return chain;
+ }
+ catch (JsonSyntaxException e)
+ {
+ LOGGER.warn("Failed to parse post shader: {}", loc, e);
+ }
+ catch (IOException e)
+ {
+ LOGGER.warn("Failed to load post shader: {}", loc, e);
+ }
+
+ return null;
+ }
+
+ public void onMainWindowResize(int width, int height)
+ {
+ this.atmoshpericClouds.onResize(width, height);
+
+ RenderTarget main = SimpleCloudsCompatHelper.getMainRenderTarget();
+ if (main == null)
+ return;
+
+ width = main.width;
+ height = main.height;
+
+ if (this.cloudTarget != null)
+ this.cloudTarget.resize(width, height, Minecraft.ON_OSX);
+
+ if (this.cloudTransparencyTarget != null)
+ this.cloudTransparencyTarget.resize(width, height, Minecraft.ON_OSX);
+
+ this.stormFogResolutionDivisor = SimpleCloudsCompatHelper.getStormFogResolutionDivisor();
+
+ if (this.stormFogTarget != null)
+ {
+ this.stormFogTarget.resize(width / this.stormFogResolutionDivisor, height / this.stormFogResolutionDivisor, Minecraft.ON_OSX);
+ this.stormFogTarget.setFilterMode(GL11.GL_LINEAR);
+ }
+
+ if (this.blurTarget != null)
+ {
+ this.blurTarget.resize(width, height, Minecraft.ON_OSX);
+ this.blurTarget.setFilterMode(GL11.GL_LINEAR);
+ }
+
+ for (PostChain chain : this.postChains)
+ {
+ RenderTarget chainTarget = ((MixinPostChain)chain).simpleclouds$getScreenTarget();
+ chain.resize(chainTarget.width, chainTarget.height);
+ }
+
+ if (this.blurPostProcessing != null)
+ this.blurPostProcessing.getTempTarget("swap").setFilterMode(GL11.GL_LINEAR);
+ }
+
+ public void shutdown()
+ {
+ if (this.cloudTarget != null)
+ this.cloudTarget.destroyBuffers();
+ if (this.cloudTransparencyTarget != null)
+ this.cloudTransparencyTarget.destroyBuffers();
+ if (this.stormFogTarget != null)
+ this.stormFogTarget.destroyBuffers();;
+ if (this.blurTarget != null)
+ this.blurTarget.destroyBuffers();
+
+ this.cloudTarget = null;
+ this.cloudTransparencyTarget = null;
+ this.stormFogTarget = null;
+ this.blurTarget = null;
+
+ this.destroyPostChains();
+
+ if (this.meshGenerator != null)
+ this.meshGenerator.close();
+
+ if (this.stormFogShadowMap != null)
+ {
+ this.stormFogShadowMap.close();
+ this.stormFogShadowMap = null;
+ }
+
+ if (this.shadowMap.isPresent())
+ {
+ this.shadowMap.get().close();
+ this.shadowMap = Optional.empty();
+ }
+
+ if (this.lightningBoltPositions != null)
+ {
+ BindingManager.freeSSBO(this.lightningBoltPositions);
+ this.lightningBoltPositions = null;
+ }
+
+ this.atmoshpericClouds.close();
+ }
+
+ public void baseTick()
+ {
+ if (this.needsReload)
+ {
+ this.onResourceManagerReload(this.mc.getResourceManager());
+ this.needsReload = false;
+ }
+ }
+
+ public void tick()
+ {
+ this.worldEffectsManager.tick();
+
+ if (this.cloudManager != null)
+ this.atmoshpericClouds.setWindDirection(this.cloudManager.calculateWindDirection());
+ this.atmoshpericClouds.tick();
+
+ if (this.meshGenerator != null)
+ this.meshGenerator.worldTick();
+ }
+
+ public static void renderCloudsOpaque(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum)
+ {
+ renderCloudsOpaque(generator, stack, projMat, fogStart, fogEnd, partialTick, r, g, b, frustum, true);
+ }
+
+ public static void renderCloudsOpaque(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum, boolean ditherFade)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ BufferUploader.reset();
+
+ if (!generator.canRender())
+ return;
+
+ RenderSystem.disableBlend();
+ RenderSystem.enableDepthTest();
+ RenderSystem.disableCull();
+
+ SingleSSBOShaderInstance shader = SimpleCloudsShaders.getCloudsShader();
+ RenderSystem.setShader(() -> shader);
+
+ TextureManager manager = Minecraft.getInstance().getTextureManager();
+ AbstractTexture ditherTexture = manager.getTexture(DITHER_TEXTURE);
+ shader.setSampler("BayerMatrixSampler", ditherTexture);
+ shader.safeGetUniform("DitherScale").set(DITHER_SCALE);
+
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+ shader.apply();
+
+ generator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, opaqueBuffers) ->
+ {
+ if (ditherFade)
+ {
+ RenderSystem.setShaderColor(r, g, b, chunk.getAlpha(partialTick));
+ shader.COLOR_MODULATOR.set(RenderSystem.getShaderColor());
+ shader.COLOR_MODULATOR.upload();
+ }
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), opaqueBuffers.getBufferId());
+ generator.getSideMesh().drawInstanced(opaqueBuffers.getElementCount());
+ }, ditherFade);
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), 0);
+
+ shader.clear();
+
+ GL30.glBindVertexArray(0);
+
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ RenderSystem.enableCull();
+ }
+
+ public static void renderCloudsTransparency(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum)
+ {
+ renderCloudsTransparency(generator, stack, projMat, fogStart, fogEnd, partialTick, r, g, b, frustum, true);
+ }
+
+ public static void renderCloudsTransparency(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float fogStart, float fogEnd, float partialTick, float r, float g, float b, @Nullable Frustum frustum, boolean ditherFade)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ BufferUploader.reset();
+
+ if (!generator.canRender() || !generator.transparencyEnabled())
+ return;
+
+ RenderSystem.enableDepthTest();
+ RenderSystem.depthMask(false);
+
+ SingleSSBOShaderInstance shader = SimpleCloudsShaders.getCloudsTransparencyShader();
+ RenderSystem.setShader(() -> shader);
+
+ TextureManager manager = Minecraft.getInstance().getTextureManager();
+ AbstractTexture ditherTexture = manager.getTexture(DITHER_TEXTURE);
+ shader.setSampler("BayerMatrixSampler", ditherTexture);
+ shader.safeGetUniform("DitherScale").set(DITHER_SCALE);
+
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+
+ shader.apply();
+
+ GL30.glEnablei(GL11.GL_BLEND, 0);
+ GL30.glEnablei(GL11.GL_BLEND, 1);
+ GL40.glBlendEquationi(0, GL14.GL_FUNC_ADD);
+ GL40.glBlendEquationi(1, GL14.GL_FUNC_ADD);
+ GL40.glBlendFunci(0, GL11.GL_ONE, GL11.GL_ONE);
+ GL40.glBlendFunci(1, GL11.GL_ZERO, GL11.GL_ONE_MINUS_SRC_COLOR);
+
+ generator.forRenderableMeshChunks(frustum, c -> c.getTransparentBuffers().get(), (chunk, transparentBuffers) ->
+ {
+ if (ditherFade)
+ {
+ RenderSystem.setShaderColor(r, g, b, chunk.getAlpha(partialTick));
+ shader.COLOR_MODULATOR.set(RenderSystem.getShaderColor());
+ shader.COLOR_MODULATOR.upload();
+ }
+
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), transparentBuffers.getBufferId());
+ generator.getCubeMesh().drawInstanced(transparentBuffers.getElementCount());
+ }, ditherFade);
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), 0);
+
+ shader.clear();
+
+ GL30.glDisablei(GL11.GL_BLEND, 0);
+ GL30.glDisablei(GL11.GL_BLEND, 1);
+ GL40.glBlendFuncSeparatei(0, GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
+ GL40.glBlendFuncSeparatei(1, GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
+
+ GL30.glBindVertexArray(0);
+
+ RenderSystem.depthMask(true);
+
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ }
+
+ private PoseStack createShadowMapStack(ShadowMapBuffer shadowMap, double camX, double camY, double camZ, Consumer transformApplier)
+ {
+ PoseStack stack = new PoseStack();
+ stack.setIdentity();
+ double depthCenter = ((double)shadowMap.getNear() + (double)shadowMap.getFar()) * -0.5D;
+ stack.translate((double)shadowMap.getViewWidth() / 2.0D, (double)shadowMap.getViewHeight() / 2.0D, depthCenter);
+ transformApplier.accept(stack);
+ float chunkSizeUpscaled = (float)SimpleCloudsConstants.CHUNK_SIZE * (float)SimpleCloudsConstants.CLOUD_SCALE;
+ float camOffsetX = ((float)Mth.floor(camX / chunkSizeUpscaled) * chunkSizeUpscaled);
+ float camOffsetZ = ((float)Mth.floor(camZ / chunkSizeUpscaled) * chunkSizeUpscaled);
+ stack.translate(-camOffsetX, -(double)this.cloudManager.getCloudHeight(), -camOffsetZ);
+ return stack;
+ }
+
+ private void renderShadowMap(ShadowMapBuffer shadowMap, PoseStack stack, SingleSSBOShaderInstance shader, @Nullable Frustum frustum)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ stack.pushPose();
+ this.translateClouds(stack, 0.0D, 0.0D, 0.0D);
+
+ RenderSystem.setShader(() -> shader);
+ prepareShader(shader, stack.last().pose(), shadowMap.getProjMatrix(), this.fogStart, this.fogEnd);
+ shader.apply();
+
+ shadowMap.bind();
+ shadowMap.clear(Minecraft.ON_OSX);
+
+ this.meshGenerator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, opaqueBuffers) ->
+ {
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), opaqueBuffers.getBufferId());
+ this.meshGenerator.getSideMesh().drawInstanced(opaqueBuffers.getElementCount());
+ });
+ GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, shader.getShaderStorageBinding(), 0);
+ GL30.glBindVertexArray(0);
+
+ shadowMap.unbind();
+
+ shader.clear();
+
+ stack.popPose();
+ }
+
+ private float determineShadowMapAngle(float partialTick)
+ {
+ float timeOfDay = this.mc.level.getTimeOfDay(partialTick);
+ return 45.0F * Mth.sin(2.0F * (float)Math.PI * timeOfDay);
+ }
+
+ private void renderShadowMaps(double camX, double camY, double camZ, float partialTick)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ BufferUploader.reset();
+
+ RenderSystem.disableBlend();
+ RenderSystem.enableDepthTest();
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ RenderSystem.disableCull();
+
+ this.stormFogShadowMapStack = this.createShadowMapStack(this.stormFogShadowMap, camX, camY, camZ, s ->
+ {
+ Vector2f direction = this.cloudManager.calculateWindDirection();
+ float yaw = (float)Mth.atan2((double)direction.x, (double)direction.y);
+ s.mulPose(Axis.XP.rotationDegrees(SimpleCloudsConfig.CLIENT.stormFogAngle.get().floatValue()));
+ s.mulPose(Axis.YP.rotation(yaw));
+ });
+ this.renderShadowMap(this.stormFogShadowMap, this.stormFogShadowMapStack, SimpleCloudsShaders.getStormFogShadowMapShader(), this.cullFrustum);
+
+ this.shadowMapStack = this.shadowMap.map(buffer ->
+ {
+ PoseStack stack = this.createShadowMapStack(buffer, camX, camY, camZ, s -> {
+ s.mulPose(Axis.XP.rotationDegrees(90.0F));
+ s.mulPose(Axis.ZN.rotationDegrees(this.determineShadowMapAngle(partialTick)));
+ });
+ this.renderShadowMap(buffer, stack, SimpleCloudsShaders.getCloudsShadowMapShader(), null);
+ return stack;
+ }).orElse(null);
+
+ RenderSystem.enableCull();
+
+ this.mc.getMainRenderTarget().bindWrite(true);
+ }
+
+ public static void renderCloudsDebug(CloudMeshGenerator generator, PoseStack stack, Matrix4f projMat, float partialTick, float fogStart, float fogEnd, @Nullable Frustum frustum, boolean chunkBoundaries, boolean noiseBoundaries)
+ {
+ RenderSystem.assertOnRenderThread();
+
+ if (!generator.canRender())
+ return;
+
+ BufferUploader.reset();
+
+ RenderSystem.disableBlend();
+ RenderSystem.enableDepthTest();
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ RenderSystem.disableCull();
+
+ Tesselator tesselator = Tesselator.getInstance();
+ BufferBuilder builder = tesselator.getBuilder();
+ builder.begin(VertexFormat.Mode.LINES, DefaultVertexFormat.POSITION_COLOR_NORMAL);
+
+ generator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, bufferSet) ->
+ {
+ PreparedChunk preparedChunk = chunk.getChunkInfo();
+ if (chunkBoundaries)
+ {
+ int color = Color.HSBtoRGB((float)preparedChunk.lodLevel() / ((float)generator.getLodConfig().getLods().length + 1), 1.0F, 1.0F);
+ float r = (float)FastColor.ARGB32.red(color) / 255.0F;
+ float g = (float)FastColor.ARGB32.green(color) / 255.0F;
+ float b = (float)FastColor.ARGB32.blue(color) / 255.0F;
+ LevelRenderer.renderLineBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getBoundsMinY() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getBoundsMaxY() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, r, g, b, 1.0F);
+ }
+ if (noiseBoundaries)
+ LevelRenderer.renderLineBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getMinHeight() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getMaxHeight() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, 1.0F, 1.0F, 0.0F, 1.0F);
+ });
+
+ RenderSystem.setShader(GameRenderer::getRendertypeLinesShader);
+ ShaderInstance shader = RenderSystem.getShader();
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+ shader.LINE_WIDTH.set(2.5F);
+ shader.FOG_START.set(Float.MAX_VALUE);
+ shader.apply();
+ BufferUploader.draw(builder.end());
+ shader.clear();
+
+ RenderSystem.enableCull();
+
+ RenderSystem.defaultBlendFunc();
+ RenderSystem.enableBlend();
+
+ builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
+
+ generator.forRenderableMeshChunks(frustum, MeshChunk::getOpaqueBuffers, (chunk, bufferSet) ->
+ {
+ PreparedChunk preparedChunk = chunk.getChunkInfo();
+ if (chunkBoundaries)
+ {
+ int color = Color.HSBtoRGB((float)preparedChunk.lodLevel() / ((float)generator.getLodConfig().getLods().length + 1), 1.0F, 1.0F);
+ float r = (float)FastColor.ARGB32.red(color) / 255.0F;
+ float g = (float)FastColor.ARGB32.green(color) / 255.0F;
+ float b = (float)FastColor.ARGB32.blue(color) / 255.0F;
+ renderChunkBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getBoundsMinY() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getBoundsMaxY() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, r, g, b, 0.4F);
+ }
+ if (noiseBoundaries)
+ renderChunkBox(builder, chunk.getBoundsMinX() + 1.0F, chunk.getMinHeight() + 1.0F, chunk.getBoundsMinZ() + 1.0F, chunk.getBoundsMaxX() - 1.0F, chunk.getMaxHeight() - 1.0F, chunk.getBoundsMaxZ() - 1.0F, 1.0F, 1.0F, 0.0F, 0.4F);
+ });
+
+ RenderSystem.setShader(GameRenderer::getPositionColorShader);
+ shader = RenderSystem.getShader();
+ SimpleCloudsRenderer.prepareShader(shader, stack.last().pose(), projMat, fogStart, fogEnd);
+ shader.apply();
+ BufferUploader.draw(builder.end());
+ shader.clear();
+
+ RenderSystem.disableBlend();
+ }
+
+ public float[] getCloudColor(float partialTick)
+ {
+ Vec3 cloudCol = this.mc.level.getCloudColor(partialTick);
+ float factor = this.worldEffectsManager.getDarkenFactor(partialTick, 0.8F);
+ float skyFlashFactor = Math.max(0.0F, ((float)this.mc.level.getSkyFlashTime() - partialTick) * SimpleCloudsConstants.LIGHTNING_FLASH_STRENGTH);
+ factor += skyFlashFactor;
+ float r = Mth.clamp((float)cloudCol.x * factor, 0.0F, 1.0F);
+ float g = Mth.clamp((float)cloudCol.y * factor, 0.0F, 1.0F);
+ float b = Mth.clamp((float)cloudCol.z * factor, 0.0F, 1.0F);
+ return new float[] { r, g, b };
+ }
+
+ public void translateClouds(PoseStack stack, double camX, double camY, double camZ)
+ {
+ stack.translate(-camX, -camY + (double)this.cloudManager.getCloudHeight(), -camZ);
+ stack.scale((float)SimpleCloudsConstants.CLOUD_SCALE, (float)SimpleCloudsConstants.CLOUD_SCALE, (float)SimpleCloudsConstants.CLOUD_SCALE);
+ }
+
+ public void renderWeather(LightTexture texture, float partialTick, double camX, double camY, double camZ)
+ {
+ if (SimpleCloudsCompatHelper.renderCustomRain())
+ this.worldEffectsManager.renderRain(texture, partialTick, camX, camY, camZ);
+ if (!SimpleCloudsMod.dhLoaded())
+ this.worldEffectsManager.renderLightning(partialTick, camX, camY, camZ);
+ }
+
+ public void renderBeforeLevel(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ CloudsRenderPipeline pipeline = CompatHelper.areShadersRunning() ? CloudsRenderPipeline.SHADER_SUPPORT : CloudsRenderPipeline.DEFAULT;
+ DetermineCloudRenderPipelineEvent pipelineEvent = new DetermineCloudRenderPipelineEvent(pipeline);
+ MinecraftForge.EVENT_BUS.post(pipelineEvent);
+ this.renderPipelineThisPass = pipeline;
+ if (pipelineEvent.getOverridenPipeline() != null)
+ this.renderPipelineThisPass = pipelineEvent.getOverridenPipeline();
+
+ float factor = this.worldEffectsManager.getDarkenFactor(partialTick);
+ float renderDistance = (float)this.meshGenerator.getCloudAreaMaxRadius() * (float)SimpleCloudsConstants.CLOUD_SCALE * factor;
+ if (renderDistance < 2867.0F)
+ renderDistance = 2867.0F;
+ ModifyCloudRenderDistanceEvent renderDistEvent = new ModifyCloudRenderDistanceEvent(renderDistance);
+ MinecraftForge.EVENT_BUS.post(renderDistEvent);
+ renderDistance = renderDistEvent.getRenderDistance();
+ this.fogStart = renderDistance / 4.0F;
+ this.fogEnd = renderDistance;
+
+ Entity cameraEntity = this.mc.gameRenderer.getMainCamera().getEntity();
+ if (cameraEntity instanceof LivingEntity living)
+ {
+ var map = living.getActiveEffectsMap();
+ if (map.containsKey(MobEffects.BLINDNESS))
+ {
+ MobEffectInstance instance = map.get(MobEffects.BLINDNESS);
+ float effectFactor = instance.isInfiniteDuration() ? 5.0F : Mth.lerp(Math.min(1.0F, (float)instance.getDuration() / 20.0F), renderDistance, 5.0F);
+ this.fogStart = 0.0F;
+ this.fogEnd = effectFactor * 0.8F;
+ }
+ else if (map.containsKey(MobEffects.DARKNESS))
+ {
+ MobEffectInstance instance = map.get(MobEffects.DARKNESS);
+ if (instance.getFactorData().isPresent())
+ {
+ float f = Mth.lerp(instance.getFactorData().get().getFactor(living, partialTick), renderDistance, 15.0F);
+ this.fogStart = 0.0F;
+ this.fogEnd = f;
+ }
+ }
+ }
+
+ this.meshGenerator.setCullDistance(this.fogEnd / (float)SimpleCloudsConstants.CLOUD_SCALE);
+
+ this.mc.getProfiler().push("simple_clouds_prepare");
+
+ this.cullFrustum = new Frustum(stack.last().pose(), projMat);
+ float scale = (float)SimpleCloudsConstants.CLOUD_SCALE;
+ double originX = camX / scale;
+ double originY = (camY - (double)this.cloudManager.getCloudHeight()) / scale;
+ double originZ = camZ / scale;
+ this.cullFrustum.prepare(originX, originY, originZ);
+
+ ProfilerFiller p = this.mc.getProfiler();
+
+ if (SimpleCloudsConfig.CLIENT.generateMesh.get() && SimpleCloudsCompatHelper.isPrimaryPass())
+ {
+ p.push("mesh_generation");
+ this.prepareMeshGenerator(partialTick);
+ this.meshGenerator.genTick(originX, originY, originZ, SimpleCloudsConfig.CLIENT.frustumCulling.get() ? this.cullFrustum : null, partialTick);
+ p.pop();
+ }
+
+ if (SimpleCloudsConfig.CLIENT.renderClouds.get() && SimpleCloudsCompatHelper.isPrimaryPass())
+ {
+ p.push("shadow_map");
+ this.renderShadowMaps(camX, camY, camZ, partialTick);
+ this.getRenderPipeline().prepare(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ p.pop();
+ }
+
+ this.mc.getProfiler().pop();
+ }
+
+ public void renderAfterSky(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ this.mc.getProfiler().push("simple_clouds_after_sky");
+ this.getRenderPipeline().afterSky(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ this.mc.getProfiler().pop();
+ }
+
+ public void renderBeforeWeather(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ this.mc.getProfiler().push("simple_clouds_before_weather");
+ this.getRenderPipeline().beforeWeather(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ this.mc.getProfiler().pop();
+ }
+
+ public void renderAfterLevel(PoseStack stack, Matrix4f projMat, float partialTick, double camX, double camY, double camZ)
+ {
+ if (!SimpleCloudsCompatHelper.renderThisPass())
+ return;
+
+ this.mc.getProfiler().push("simple_clouds");
+ this.getRenderPipeline().afterLevel(this.mc, this, stack, projMat, partialTick, camX, camY, camZ, this.cullFrustum);
+ this.mc.getProfiler().pop();
+
+ this.mc.getProfiler().push("world_effects");
+ this.worldEffectsManager.renderPost(stack, partialTick, camX, camY, camZ, (float)SimpleCloudsConstants.CLOUD_SCALE);
+ this.mc.getProfiler().pop();
+ }
+
+ public void doBlurPostProcessing(float partialTick)
+ {
+ if (this.blurPostProcessing != null)
+ {
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.disableBlend();
+ RenderSystem.depthMask(false);
+ this.blurPostProcessing.process(partialTick);
+ RenderSystem.depthMask(true);
+ }
+ }
+
+ public void doScreenSpaceWorldFog(PoseStack stack, Matrix4f projMat, float partialTick)
+ {
+ if (this.screenSpaceWorldFog != null)
+ {
+ RenderSystem.disableBlend();
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ Matrix4f invertedProjMat = new Matrix4f(projMat).invert();
+ Matrix4f invertedModelViewMat = new Matrix4f(stack.last().pose()).invert();
+ for (PostPass pass : ((MixinPostChain)this.screenSpaceWorldFog).simpleclouds$getPostPasses())
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.safeGetUniform("InverseWorldProjMat").set(invertedProjMat);
+ effect.safeGetUniform("InverseModelViewMat").set(invertedModelViewMat);
+ effect.safeGetUniform("FogStart").set(RenderSystem.getShaderFogStart());
+ effect.safeGetUniform("FogEnd").set(RenderSystem.getShaderFogEnd());
+ float[] fogCol = RenderSystem.getShaderFogColor();
+ effect.safeGetUniform("FogColor").set(fogCol[0], fogCol[1], fogCol[2]);
+ effect.safeGetUniform("FogShape").set(RenderSystem.getShaderFogShape().getIndex());
+ }
+
+ this.screenSpaceWorldFog.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+ }
+
+ public void doFinalCompositePass(PoseStack stack, float partialTick, Matrix4f projMat)
+ {
+ if (this.finalComposite != null)
+ {
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ this.finalComposite.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+ }
+
+ public void doStormPostProcessing(PoseStack stack, float partialTick, Matrix4f projMat, double camX, double camY, double camZ, float r, float g, float b)
+ {
+ if (this.stormPostProcessing == null || this.stormFogShadowMapStack == null || this.stormFogShadowMapStack == null)
+ return;
+
+ RenderSystem.disableBlend();
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ this.stormFogTarget.clear(Minecraft.ON_OSX);
+ this.stormFogTarget.bindWrite(true);
+
+ MutableInt size = new MutableInt();
+ boolean flag = SimpleCloudsConfig.CLIENT.stormFogLightningFlashes.get();
+ if (flag)
+ {
+ List lightningBolts = this.worldEffectsManager.getLightningBolts();
+ size.setValue(Math.min(lightningBolts.size(), MAX_LIGHTNING_BOLTS));
+ if (size.getValue() > 0)
+ {
+ this.lightningBoltPositions.writeData(buffer ->
+ {
+ for (int i = 0; i < size.getValue(); i++)
+ {
+ LightningBolt bolt = lightningBolts.get(i);
+ Vector3f pos = bolt.getPosition();
+ buffer.putFloat(pos.x);
+ buffer.putFloat(pos.y);
+ buffer.putFloat(pos.z);
+ buffer.putFloat(bolt.getFade(partialTick));
+ }
+ buffer.rewind();
+ }, size.getValue() * BYTES_PER_LIGHTNING_BOLT, false);
+ }
+ }
+
+ Matrix4f invertedProjMat = new Matrix4f(projMat).invert();
+ Matrix4f invertedModelViewMat = new Matrix4f(stack.last().pose()).invert();
+ for (PostPass pass : ((MixinPostChain)this.stormPostProcessing).simpleclouds$getPostPasses())
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.safeGetUniform("InverseWorldProjMat").set(invertedProjMat);
+ effect.safeGetUniform("InverseModelViewMat").set(invertedModelViewMat);
+ effect.safeGetUniform("ShadowProjMat").set(this.stormFogShadowMap.getProjMatrix());
+ effect.safeGetUniform("ShadowModelViewMat").set(this.stormFogShadowMapStack.last().pose());
+ effect.safeGetUniform("CameraPos").set((float)camX, (float)camY, (float)camZ);
+ effect.safeGetUniform("FogStart").set(this.fogEnd / 2.0F);
+ effect.safeGetUniform("FogEnd").set(this.fogEnd);
+ effect.safeGetUniform("ColorModulator").set(r, g, b, 1.0F);
+ float factor = this.worldEffectsManager.getDarkenFactor(partialTick);
+ effect.safeGetUniform("CutoffDistance").set(1000.0F * factor);
+ effect.safeGetUniform("TotalLightningBolts").set(size.getValue());
+ }
+
+ this.stormPostProcessing.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+
+ public void doCloudShadowProcessing(PoseStack stack, float partialTick, Matrix4f projMat, double camX, double camY, double camZ, int depthBufferId)
+ {
+ if (this.cloudShadows == null || this.shadowMap.isEmpty() || this.shadowMapStack == null)
+ return;
+
+ RenderSystem.disableBlend();
+ RenderSystem.disableDepthTest();
+ RenderSystem.resetTextureMatrix();
+ RenderSystem.depthMask(false);
+
+ Matrix4f invertedProjMat = new Matrix4f(projMat).invert();
+ Matrix4f invertedModelViewMat = new Matrix4f(stack.last().pose()).invert();
+ float minimumRadius = this.mc.gameRenderer.getRenderDistance();
+ for (PostPass pass : ((MixinPostChain)this.cloudShadows).simpleclouds$getPostPasses())
+ {
+ EffectInstance effect = pass.getEffect();
+ effect.setSampler("DepthSampler", () -> depthBufferId);
+ effect.safeGetUniform("InverseWorldProjMat").set(invertedProjMat);
+ effect.safeGetUniform("InverseModelViewMat").set(invertedModelViewMat);
+ effect.safeGetUniform("ShadowProjMat").set(this.shadowMap.get().getProjMatrix());
+ effect.safeGetUniform("ShadowModelViewMat").set(this.shadowMapStack.last().pose());
+ effect.safeGetUniform("CameraPos").set((float)camX, (float)camY, (float)camZ);
+ effect.safeGetUniform("MinimumRadius").set(minimumRadius);
+ }
+
+ this.cloudShadows.process(partialTick);
+
+ RenderSystem.depthMask(true);
+ }
+
+ public static void prepareShader(ShaderInstance shader, Matrix4f modelView, Matrix4f projMat, float fogStart, float fogEnd)
+ {
+ for (int i = 0; i < 12; ++i)
+ {
+ int j = RenderSystem.getShaderTexture(i);
+ shader.setSampler("Sampler" + i, j);
+ }
+
+ if (shader.MODEL_VIEW_MATRIX != null)
+ shader.MODEL_VIEW_MATRIX.set(modelView);
+
+ if (shader.PROJECTION_MATRIX != null)
+ shader.PROJECTION_MATRIX.set(projMat);
+
+ if (shader.INVERSE_VIEW_ROTATION_MATRIX != null)
+ shader.INVERSE_VIEW_ROTATION_MATRIX.set(RenderSystem.getInverseViewRotationMatrix());
+
+ if (shader.COLOR_MODULATOR != null)
+ shader.COLOR_MODULATOR.set(RenderSystem.getShaderColor());
+
+ if (shader.GLINT_ALPHA != null)
+ shader.GLINT_ALPHA.set(RenderSystem.getShaderGlintAlpha());
+
+ if (shader.FOG_START != null)
+ shader.FOG_START.set(fogStart);
+
+ if (shader.FOG_END != null)
+ shader.FOG_END.set(fogEnd);
+
+ if (shader.FOG_COLOR != null)
+ shader.FOG_COLOR.set(RenderSystem.getShaderFogColor());
+
+ if (shader.FOG_SHAPE != null)
+ shader.FOG_SHAPE.set(RenderSystem.getShaderFogShape().getIndex());
+
+ if (shader.TEXTURE_MATRIX != null)
+ shader.TEXTURE_MATRIX.set(RenderSystem.getTextureMatrix());
+
+ if (shader.GAME_TIME != null)
+ shader.GAME_TIME.set(RenderSystem.getShaderGameTime());
+
+ if (shader.SCREEN_SIZE != null)
+ {
+ Window window = Minecraft.getInstance().getWindow();
+ shader.SCREEN_SIZE.set((float) window.getWidth(), (float) window.getHeight());
+ }
+
+ shader.safeGetUniform("UseNormals").set(SimpleCloudsConfig.CLIENT.cubeNormals.get() ? 1 : 0);
+
+ RenderSystem.setShaderLights(DIFFUSE_LIGHT_0, DIFFUSE_LIGHT_1);
+ RenderSystem.setupShaderLights(shader);
+ }
+
+ public void copyDepthFromCloudsToMain()
+ {
+ this._copyDepthSafe(this.mc.getMainRenderTarget(), this.cloudTarget);
+ }
+
+ public void copyDepthFromMainToClouds()
+ {
+ this._copyDepthSafe(this.cloudTarget, this.mc.getMainRenderTarget());
+ }
+
+ public void copyDepthFromCloudsToTransparency()
+ {
+ this._copyDepthSafe(this.cloudTransparencyTarget, this.cloudTarget);
+ }
+
+ private void _copyDepthSafe(RenderTarget to, RenderTarget from)
+ {
+ RenderSystem.assertOnRenderThread();
+ GlStateManager._getError(); //Clear old error
+ if (!this.failedToCopyDepthBuffer)
+ {
+ to.bindWrite(false);
+ to.copyDepthFrom(from);
+ if (GlStateManager._getError() != GL11.GL_INVALID_OPERATION)
+ return;
+ boolean enabledStencil = false;
+ if (to.isStencilEnabled() && !from.isStencilEnabled())
+ {
+ from.enableStencil();
+ enabledStencil = true;
+ }
+ else if (from.isStencilEnabled() && !to.isStencilEnabled())
+ {
+ to.enableStencil();
+ enabledStencil = true;
+ }
+ if (enabledStencil)
+ {
+ to.copyDepthFrom(from);
+ if (GlStateManager._getError() == GL11.GL_INVALID_OPERATION)
+ {
+ LOGGER.error("Unable to copy depth between the main and clouds frame buffers, even after enabling stencil. Please note that the clouds may not render properly.");
+ this.failedToCopyDepthBuffer = true;
+ }
+ else
+ {
+ LOGGER.info("NOTE: Please ignore the above OpenGL error. Simple Clouds had to toggle stencil in order to copy the depth buffer between the main and clouds frame buffers.");
+ }
+ }
+ else
+ {
+ LOGGER.error("Unable to copy depth between the main and clouds frame buffers. Please note that the clouds may not render properly.");
+ this.failedToCopyDepthBuffer = true;
+ }
+ }
+ }
+
+ public void fillReport(CrashReport report)
+ {
+ CrashReportCategory category = report.addCategory("Simple Clouds Renderer");
+ category.setDetail("Cloud Mode", this.settings.getCurrentCloudMode());
+ category.setDetail("Cloud Target Available", this.cloudTarget != null);
+ category.setDetail("Storm Fog Target Active", this.stormFogTarget != null);
+ category.setDetail("Blur Target Active", this.blurTarget != null);
+ category.setDetail("Transparency Target Active", this.cloudTransparencyTarget != null);
+ category.setDetail("Post Chains", this.postChains.toString());
+ category.setDetail("Lightning Bolt SSBO", this.lightningBoltPositions);
+ category.setDetail("Clouds Shadow Map", this.stormFogShadowMap);
+ category.setDetail("Storm Fog Shadow Map", this.stormFogShadowMap);
+ category.setDetail("Failed to copy depth buffer", this.failedToCopyDepthBuffer);
+ category.setDetail("Needs Reload", this.needsReload);
+
+ CrashReportCategory meshGenCategory = report.addCategory("Cloud Mesh Generator");
+ if (this.meshGenerator != null)
+ {
+ meshGenCategory.setDetail("Type", this.meshGenerator.toString());
+ this.meshGenerator.fillReport(meshGenCategory);
+ }
+ else
+ {
+ meshGenCategory.setDetail("Type", "Mesh generator is not initialized");
+ }
+ }
+
+ public static void initialize(CloudsRendererSettings settings)
+ {
+ RenderSystem.assertOnRenderThread();
+ if (instance != null)
+ throw new IllegalStateException("Simple Clouds renderer is already initialized");
+ instance = new SimpleCloudsRenderer(settings, Minecraft.getInstance());
+ LOGGER.debug("Clouds render initialized");
+ }
+
+ public static SimpleCloudsRenderer getInstance()
+ {
+ return Objects.requireNonNull(instance, "Renderer not initialized!");
+ }
+
+ public static Optional getOptionalInstance()
+ {
+ return Optional.ofNullable(instance);
+ }
+
+ public static boolean canRenderInDimension(@Nullable ClientLevel level)
+ {
+ if (level == null)
+ return false;
+
+ List extends String> whitelist;
+ boolean useAsBlacklist;
+ if (ClientCloudManager.isAvailableServerSide() && SimpleCloudsConfig.SERVER_SPEC.isLoaded())
+ {
+ whitelist = SimpleCloudsConfig.SERVER.dimensionWhitelist.get();
+ useAsBlacklist = SimpleCloudsConfig.SERVER.whitelistAsBlacklist.get();
+ }
+ else
+ {
+ whitelist = SimpleCloudsConfig.CLIENT.dimensionWhitelist.get();
+ useAsBlacklist = SimpleCloudsConfig.CLIENT.whitelistAsBlacklist.get();
+ }
+
+ boolean flag = whitelist.stream().anyMatch(val -> {
+ return level.dimension().location().toString().equals(val);
+ });
+
+ return useAsBlacklist ? !flag : flag;
+ }
+
+ private static void renderChunkBox(VertexConsumer consumer, float minX, float minY, float minZ, float maxX, float maxY, float maxZ, float r, float g, float b, float a)
+ {
+ //-X
+ consumer.vertex(minX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, minZ).color(r, g, b, a).endVertex();
+
+ //+X
+ consumer.vertex(maxX, minY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, minY, maxZ).color(r, g, b, a).endVertex();
+
+ //-Y
+ consumer.vertex(maxX, minY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, minZ).color(r, g, b, a).endVertex();
+
+ //+Y
+ consumer.vertex(minX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, minZ).color(r, g, b, a).endVertex();
+
+ //-Z
+ consumer.vertex(minX, minY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, minZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, minY, minZ).color(r, g, b, a).endVertex();
+
+ //+Z
+ consumer.vertex(maxX, minY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(maxX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, maxY, maxZ).color(r, g, b, a).endVertex();
+ consumer.vertex(minX, minY, maxZ).color(r, g, b, a).endVertex();
+ }
+
+ private static int calculateMeshGenInterval()
+ {
+ int fps = Minecraft.getInstance().getFps();
+ switch (SimpleCloudsConfig.CLIENT.generationInterval.get())
+ {
+ case STATIC:
+ {
+ return SimpleCloudsConfig.CLIENT.framesToGenerateMesh.get();
+ }
+ case DYNAMIC:
+ {
+ return Math.max(Mth.ceil((130.0F - (float)fps) / 30.0F) + 5, 1);
+ }
+ case TARGET_FPS:
+ {
+ return Math.max(Mth.ceil((float)fps / SimpleCloudsConfig.CLIENT.targetMeshGenFps.get()), 1);
+ }
+ default:
+ return 5;
+ }
+ }
+}
diff --git a/upstream_sc/assets/simpleclouds/shaders/compute/cloud_regions.comp b/upstream_sc/assets/simpleclouds/shaders/compute/cloud_regions.comp
new file mode 100644
index 00000000..1e2d9ffd
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/compute/cloud_regions.comp
@@ -0,0 +1,82 @@
+#version 430
+
+#define EFF ${EDGE_FADE_FACTOR} // Edge Fade Factor
+
+layout(local_size_x = ${LOCAL_SIZE_X}, local_size_y = ${LOCAL_SIZE_Y}, local_size_z = ${LOCAL_SIZE_Z}) in;
+
+struct CloudRegion {
+ float posX;
+ float posZ;
+ float index;
+ float radius;
+ mat2 transform;
+};
+
+layout(std430) readonly buffer CloudRegions {
+ CloudRegion data[];
+}
+cloudRegions;
+
+layout(std430) restrict readonly buffer LodScales {
+ float data[];
+}
+lodScales;
+
+layout(rg32f) restrict writeonly uniform image2DArray regionTexture;
+
+uniform int TotalCloudRegions;
+uniform vec2 Offset;
+
+vec3 circle(CloudRegion region, vec2 coord)
+{
+ vec2 p = vec2(region.posX, region.posZ);
+ coord = region.transform * (coord - p) + p;
+ float d = distance(p, coord);
+ float r = region.radius;
+ if (d > r + 1.0 / EFF)
+ return vec3(-1.0);
+ else if (d < r)
+ return vec3(min((r - d) * EFF, 1.0), 0.0, region.index);
+ else
+ return vec3(0.0, min((d - r) * EFF, 1.0), region.index);
+}
+
+vec2 composite(vec2 old, vec3 data)
+{
+ if (data.r > 0.0)
+ {
+ if (old.r >= 0.0 && old.r == data.b)
+ return vec2(old.r, mix(old.g, 1.0, data.r));
+ else
+ return data.br;
+ }
+ else if (data.g >= 0.0)
+ {
+ if (old.r >= 0.0 && old.r == data.b)
+ return old;
+ else
+ return vec2(old.r, old.g * data.g);
+ }
+ else
+ {
+ return old;
+ }
+}
+
+void main()
+{
+ uint lod = gl_GlobalInvocationID.z;
+ float coordScale = lodScales.data[lod];
+ vec2 centerOffset = imageSize(regionTexture).xy / 2.0;
+ ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);
+ vec2 coord = (gl_GlobalInvocationID.xy - centerOffset) * coordScale + Offset;
+
+ vec2 result = vec2(0.0);
+ for (int i = 0; i < TotalCloudRegions; i++)
+ {
+ vec3 data = circle(cloudRegions.data[i], coord);
+ result = composite(result, data);
+ }
+
+ imageStore(regionTexture, ivec3(texelCoord, lod), vec4(result, 0.0, 0.0));
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/compute/cube_mesh.comp b/upstream_sc/assets/simpleclouds/shaders/compute/cube_mesh.comp
new file mode 100644
index 00000000..ae7b0d9d
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/compute/cube_mesh.comp
@@ -0,0 +1,409 @@
+#version 430
+
+#define TYPE ${TYPE} //0 for multi-region, 1 for single cloud type
+#define FADE_NEAR_ORIGIN ${FADE_NEAR_ORIGIN} //0 to disable, 1 to enable
+#define STYLE ${STYLE} //0 for default, 1 for shaded
+#define TRANSPARENCY ${TRANSPARENCY} //0 for no transparency, 1 for transparency
+#define FIXED_SECTION_SIZE ${FIXED_SECTION_SIZE} //0 for false, 1 for true
+
+#define SHADE_DIRECTION normalize(vec3(0.5, 0.5, 0.5))
+
+#define TILE_PERIOD vec3(32.0, 64.0, 32.0)
+
+#define LOCAL_SIZE vec3(${LOCAL_SIZE_X}, ${LOCAL_SIZE_Y}, ${LOCAL_SIZE_Z})
+layout(local_size_x = ${LOCAL_SIZE_X}, local_size_y = ${LOCAL_SIZE_Y}, local_size_z = ${LOCAL_SIZE_Z}) in;
+
+#moj_import
+
+struct LayerGroup {
+ int StartIndex;
+ int EndIndex;
+ float Storminess;
+ float StormStart;
+ float StormFadeDistance;
+ float TransparencyFade;
+};
+
+struct NoiseLayer {
+ float Height;
+ float ValueOffset;
+ float ScaleX;
+ float ScaleY;
+ float ScaleZ;
+ float FadeDistance;
+ float HeightOffset;
+ float ValueScale;
+};
+
+// ----- Opaque -----
+
+struct SideInfo {
+ int side;
+ float x;
+ float y;
+ float z;
+ float brightness;
+ float radius;
+};
+
+// ----- Transparent -----
+
+#if TRANSPARENCY == 1
+
+struct TransparentCubeInfo {
+ float x;
+ float y;
+ float z;
+ float brightness;
+ float alpha;
+ float radius;
+};
+
+#endif
+
+// -----------------------
+
+//Faces:
+//-X = 0
+//+X = 1
+//-Y = 2
+//+Y = 3
+//-Z = 4
+//+Z = 5
+
+//const uint sideIndices[6] = {
+// 0, 1, 2, 0, 2, 3
+//};
+//
+//#if TRANSPARENCY == 1
+//
+//const uint transparentCubeIndices[36] = {
+// 0, 1, 2, 0, 2, 3, //-z
+// 4, 7, 6, 4, 6, 5, //+z
+// 7, 0, 3, 7, 3, 6, //-x
+// 1, 4, 5, 1, 5, 2, //+x
+// 1, 0, 7, 1, 7, 4, //-y
+// 5, 6, 3, 5, 3, 2 //+y
+//};
+//
+//#endif
+
+// ----- Opaque Buffers -----
+
+#if FIXED_SECTION_SIZE == 0
+layout(std430) restrict buffer TotalSides {
+ uint totalSides;
+};
+#endif
+
+layout(std430) restrict writeonly buffer SideInfoBuffer {
+ SideInfo data[];
+}
+sides;
+
+layout(std430) restrict buffer SidesPerChunk {
+ uint data[];
+}
+sidesPerChunk;
+
+// ----- Transparency Buffers -----
+
+#if TRANSPARENCY == 1
+
+#if FIXED_SECTION_SIZE == 0
+layout(std430) restrict buffer TotalTransparentCubes {
+ uint totalTransparentCubes;
+};
+#endif
+
+layout(std430) restrict writeonly buffer TransparentCubeInfoBuffer {
+ TransparentCubeInfo data[];
+}
+cubesTransparent;
+
+layout(std430) restrict buffer TransparentCubesPerChunk {
+ uint data[];
+}
+transparentCubesPerChunk;
+
+#endif
+
+// ----------------------------------
+
+layout(std430) readonly buffer NoiseLayers {
+ NoiseLayer data[];
+}
+layers;
+
+layout(std430) readonly buffer LayerGroupings {
+ LayerGroup data[];
+}
+layerGroupings;
+
+#if TYPE == 0
+uniform sampler2DArray RegionsSampler;
+uniform int RegionsTexSize;
+#endif
+
+uniform int LodLevel;
+uniform int TotalLodLevels;
+uniform vec3 RenderOffset;
+uniform float Scale = 1.0;
+uniform vec3 Scroll;
+uniform float Wiggle;
+uniform vec3 Origin;
+uniform bool TestFacesFacingAway;
+uniform int DoNotOccludeSide = -1;
+uniform int ChunkIndex;
+
+#if FIXED_SECTION_SIZE == 1
+//Offset in number of mesh elements
+uniform int OpaqueMeshDataOffset;
+uniform int TransparentMeshDataOffset;
+#endif
+
+#if TRANSPARENCY == 1
+uniform int TransparencyDistance = 300;
+#endif
+
+#if TYPE == 0
+uniform vec2 RegionSampleOffset;
+#elif TYPE == 1
+uniform float FadeStart;
+uniform float FadeEnd;
+#endif
+
+#if FADE_NEAR_ORIGIN == 1
+uniform float FadeStart;
+uniform float FadeEnd;
+#endif
+
+float getNoiseForLayer(NoiseLayer layer, float x, float y, float z, out vec3 gradient)
+{
+ if (y < layer.HeightOffset || y > layer.HeightOffset + layer.Height - 1)
+ return -10000.0;
+ vec3 scale = vec3(layer.ScaleX, layer.ScaleY, layer.ScaleZ);
+ vec3 scrollScaled = Scroll / scale;
+ vec3 samplePos = vec3(x, y, z) / scale + scrollScaled;
+ float noise = psrdnoise(samplePos, TILE_PERIOD, Wiggle, gradient) * layer.ValueScale + layer.ValueOffset;
+ float heightDelta = y - layer.HeightOffset;
+ noise -= 1.0 - clamp(heightDelta / layer.FadeDistance, 0.0, 1.0);
+ noise -= 1.0 - clamp((layer.Height - heightDelta) / layer.FadeDistance, 0.0, 1.0);
+ return noise;
+}
+
+float getNoiseForLayerGroup(LayerGroup group, float x, float y, float z, out vec3 gradient)
+{
+ int totalLayers = group.EndIndex - group.StartIndex;
+ if (totalLayers == 0)
+ {
+ return -10000.0;
+ }
+ else if (totalLayers == 1)
+ {
+ return getNoiseForLayer(layers.data[group.StartIndex], x, y, z, gradient);
+ }
+ else
+ {
+ vec3 finalGradient = vec3(0.0);
+ float combinedNoise = 0.0;
+ bool anyValid = false;
+ for (int i = 0; i < totalLayers; i++)
+ {
+ vec3 gradForLayer = vec3(0.0);
+ float valForLayer = getNoiseForLayer(layers.data[i + group.StartIndex], x, y, z, gradForLayer);
+ if (valForLayer > -10.0)
+ {
+ combinedNoise += valForLayer;
+ finalGradient += gradForLayer;
+ anyValid = true;
+ }
+ }
+ gradient = finalGradient;
+ if (anyValid)
+ return combinedNoise;
+ else
+ return -10000.0;
+ }
+ return 0.0;
+}
+
+bool isPosValid(float x, float y, float z, LayerGroup group, float fade)
+{
+ vec3 gradient = vec3(0.0);
+ return getNoiseForLayerGroup(group, x, y, z, gradient) + fade > 0.0;
+}
+
+bool isPosValid(float x, float y, float z, int nx, int nz)
+{
+#if TYPE == 0
+ vec2 texelCoord = gl_GlobalInvocationID.xz + RegionSampleOffset + vec2(nx, nz);
+ vec4 info = texture(RegionsSampler, vec3(texelCoord / RegionsTexSize, float(LodLevel)));
+ uint regionId = uint(info.r);
+ LayerGroup group = layerGroupings.data[regionId];
+ float fade = -5.0 * pow(1.0 - info.g, 10.0);
+#if FADE_NEAR_ORIGIN == 1
+ float len = distance(vec2(x, z), Origin.xz);
+ fade = min(fade, -5.0 * (1.0 - min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0)));
+#endif
+#elif TYPE == 1
+ LayerGroup group = layerGroupings.data[0];
+ float len = distance(vec2(x, z), Origin.xz);
+ float fade = -5.0 * min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0);
+#endif
+ return isPosValid(x, y, z, group, fade);
+}
+
+bool shouldNotOcclude(int index)
+{
+ if (DoNotOccludeSide != -1 && index == DoNotOccludeSide)
+ {
+ vec3 id = gl_GlobalInvocationID;
+ vec3 size = gl_NumWorkGroups * LOCAL_SIZE;
+ if (DoNotOccludeSide == 1)
+ return id.x == size.x - 1.0;
+ else if (DoNotOccludeSide == 0)
+ return id.x == 0.0;
+ else if (DoNotOccludeSide == 3)
+ return id.y == size.y - 1.0;
+ else if (DoNotOccludeSide == 2)
+ return id.y == 0.0;
+ else if (DoNotOccludeSide == 5)
+ return id.z == size.z - 1.0;
+ else if (DoNotOccludeSide == 4)
+ return id.z == 0.0;
+ else
+ return false;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+// ----- Opaque -----
+
+void createFace(vec3 center, float cubeRadius, int index, float brightness)
+{
+#if FIXED_SECTION_SIZE == 0
+ atomicAdd(sidesPerChunk.data[ChunkIndex], 1u);
+ uint currentFace = atomicAdd(totalSides, 1u);
+#elif FIXED_SECTION_SIZE == 1
+ uint currentFace = atomicAdd(sidesPerChunk.data[ChunkIndex], 1u);
+#endif
+ SideInfo side;
+ side.side = index;
+ side.x = center.x;
+ side.y = center.y;
+ side.z = center.z;
+ side.brightness = brightness;
+ side.radius = cubeRadius;
+#if FIXED_SECTION_SIZE == 0
+ sides.data[currentFace] = side;
+#elif FIXED_SECTION_SIZE == 1
+ sides.data[OpaqueMeshDataOffset + currentFace] = side;
+#endif
+}
+
+void createCube(float x, float y, float z, float cubeRadius, float brightness, float fade, LayerGroup group)
+{
+ vec3 pos = vec3(x, y, z);
+ vec3 norm = normalize(pos - Origin);
+ vec3 center = pos + cubeRadius;
+ //-Y
+ if ((TestFacesFacingAway || dot(norm, vec3(0.0, -1.0, 0.0)) <= 0.0) && (!isPosValid(x, y - Scale, z, group, fade) || shouldNotOcclude(2)))
+ createFace(center, cubeRadius, 2, brightness);
+ //+Y
+ if ((TestFacesFacingAway || dot(norm, vec3(0.0, 1.0, 0.0)) <= 0.0) && (!isPosValid(x, y + Scale, z, group, fade) || shouldNotOcclude(3)))
+ createFace(center, cubeRadius, 3, brightness);
+ //-X
+ if ((TestFacesFacingAway || dot(norm, vec3(-1.0, 0.0, 0.0)) <= 0.0) && (!isPosValid(x - Scale, y, z, -1, 0) || shouldNotOcclude(0)))
+ createFace(center, cubeRadius, 0, brightness);
+ //+X
+ if ((TestFacesFacingAway || dot(norm, vec3(1.0, 0.0, 0.0)) <= 0.0) && (!isPosValid(x + Scale, y, z, 1, 0) || shouldNotOcclude(1)))
+ createFace(center, cubeRadius, 1, brightness);
+ //-Z
+ if ((TestFacesFacingAway || dot(norm, vec3(0.0, 0.0, -1.0)) <= 0.0) && (!isPosValid(x, y, z - Scale, 0, -1) || shouldNotOcclude(4)))
+ createFace(center, cubeRadius, 4, brightness);
+ //+Z
+ if ((TestFacesFacingAway || dot(norm, vec3(0.0, 0.0, 1.0)) <= 0.0) && (!isPosValid(x, y, z + Scale, 0, 1) || shouldNotOcclude(5)))
+ createFace(center, cubeRadius, 5, brightness);
+}
+
+// ----- Transparent -----
+
+#if TRANSPARENCY == 1
+
+void createTransparentCube(float x, float y, float z, float cubeRadius, float brightness, float alpha)
+{
+#if FIXED_SECTION_SIZE == 0
+ atomicAdd(transparentCubesPerChunk.data[ChunkIndex], 1u);
+ uint currentCube = atomicAdd(totalTransparentCubes, 1u);
+#elif FIXED_SECTION_SIZE == 1
+ uint currentCube = atomicAdd(transparentCubesPerChunk.data[ChunkIndex], 1u);
+#endif
+ TransparentCubeInfo cube;
+ cube.x = x + cubeRadius;
+ cube.y = y + cubeRadius;
+ cube.z = z + cubeRadius;
+ cube.brightness = brightness;
+ cube.alpha = alpha;
+ cube.radius = cubeRadius;
+#if FIXED_SECTION_SIZE == 0
+ cubesTransparent.data[currentCube] = cube;
+#elif FIXED_SECTION_SIZE == 1
+ cubesTransparent.data[TransparentMeshDataOffset + currentCube] = cube;
+#endif
+}
+
+#endif
+
+// -----------------------
+
+void main()
+{
+ vec3 id = gl_GlobalInvocationID;
+ float x = id.x * Scale + RenderOffset.x;
+ float y = id.y * Scale + RenderOffset.y;
+ float z = id.z * Scale + RenderOffset.z;
+
+#if TYPE == 0
+ vec2 texelCoord = gl_GlobalInvocationID.xz + RegionSampleOffset;
+ vec4 info = texture(RegionsSampler, vec3(texelCoord / RegionsTexSize, float(LodLevel)));
+ uint regionId = uint(info.r);
+ LayerGroup group = layerGroupings.data[regionId];
+ float fade = -5.0 * pow(1.0 - info.g, 10.0);
+#if FADE_NEAR_ORIGIN == 1
+ float len = distance(vec2(x, z), Origin.xz);
+ fade = min(fade, -5.0 * (1.0 - min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0)));
+#endif
+#elif TYPE == 1
+ LayerGroup group = layerGroupings.data[0];
+ float len = distance(vec2(x, z), Origin.xz);
+ float fade = -5.0 * min(max(len - FadeStart, 0.0) / (FadeEnd - FadeStart), 1.0);
+#endif
+ vec3 gradient = vec3(0.0);
+ float noise = getNoiseForLayerGroup(group, x, y, z, gradient) + fade;
+ float storminess = clamp(group.Storminess + fade * 0.1, 0.0, 1.0);
+ float brightness = clamp(1.0 - storminess * (1.0 - clamp((y - group.StormStart) / group.StormFadeDistance, 0.0, 1.0)), 0.0, 1.0);
+#if STYLE == 1
+ gradient = normalize(gradient);
+ float strength = dot(gradient, SHADE_DIRECTION) * 0.5 + 0.5;
+ brightness = clamp(brightness - strength * 0.1, 0.0, 1.0);
+#endif
+ if (noise > 0.0)
+ {
+ createCube(x, y, z, Scale / 2.0, brightness, fade, group);
+ }
+#if TRANSPARENCY == 1
+ else if (group.TransparencyFade > 0.01 && noise > -group.TransparencyFade && noise < 0.0)
+ {
+ float length = distance(vec2(x, z), Origin.xz);
+ if (length < TransparencyDistance)
+ {
+ float alpha = 1.0 / group.TransparencyFade * (noise + group.TransparencyFade);
+ createTransparentCube(x, y, z, Scale / 2.0, brightness, alpha);
+ }
+ }
+#endif
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/core/cloud_region_tex.json b/upstream_sc/assets/simpleclouds/shaders/core/cloud_region_tex.json
new file mode 100644
index 00000000..ce334106
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/core/cloud_region_tex.json
@@ -0,0 +1,23 @@
+{
+ "blend": {
+ "func": "add",
+ "srcrgb": "srcalpha",
+ "dstrgb": "1-srcalpha"
+ },
+ "vertex": "simpleclouds:cloud_region_tex",
+ "fragment": "simpleclouds:cloud_region_tex",
+ "attributes": [
+ "Position",
+ "UV0"
+ ],
+ "samplers": [
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ { "name": "LodLevel", "type": "int", "count": 1, "values": [ 0 ] },
+ { "name": "TotalCloudTypes", "type": "int", "count": 1, "values": [ 1 ] },
+ { "name": "Align", "type": "float", "count": 2, "values": [ 0.0, 0.0 ]}
+ ]
+}
diff --git a/upstream_sc/assets/simpleclouds/shaders/core/clouds.json b/upstream_sc/assets/simpleclouds/shaders/core/clouds.json
new file mode 100644
index 00000000..14e3e286
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/core/clouds.json
@@ -0,0 +1,25 @@
+{
+ "vertex": "simpleclouds:clouds",
+ "fragment": "simpleclouds:clouds",
+ "attributes": [
+ "Position"
+ ],
+ "samplers": [
+ { "name": "BayerMatrixSampler" }
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ { "name": "Light0_Direction", "type": "float", "count": 3, "values": [0.0, 0.0, 0.0] },
+ { "name": "Light1_Direction", "type": "float", "count": 3, "values": [0.0, 0.0, 0.0] },
+ { "name": "LightPower", "type": "float", "count": 1, "values": [0.4] },
+ { "name": "AmbientLight", "type": "float", "count": 1, "values": [0.9] },
+ { "name": "DarknessColorModifier", "type": "float", "count": 3, "values": [ 0.0, 0.0, 0.15 ] },
+ { "name": "UseNormals", "type": "int", "count": 1, "values": [ 1.0 ]},
+ { "name": "DitherScale", "type": "float", "count": 1, "values": [ 0.05 ] },
+ { "name": "FogStart", "type": "float", "count": 1, "values": [ 1.0 ] },
+ { "name": "FogEnd", "type": "float", "count": 1, "values": [ 2.0 ] },
+ { "name": "FogColor", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }
+ ]
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/core/clouds_shadow_map.json b/upstream_sc/assets/simpleclouds/shaders/core/clouds_shadow_map.json
new file mode 100644
index 00000000..56602a67
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/core/clouds_shadow_map.json
@@ -0,0 +1,15 @@
+{
+ "vertex": "simpleclouds:clouds_shadow_map",
+ "fragment": "simpleclouds:clouds_shadow_map",
+ "attributes": [
+ "Position"
+ ],
+ "samplers": [
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ { "name": "HeightCutoff", "type": "float", "count": 1, "values": [ 128.0 ] }
+ ]
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/core/clouds_transparency.json b/upstream_sc/assets/simpleclouds/shaders/core/clouds_transparency.json
new file mode 100644
index 00000000..346ccaed
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/core/clouds_transparency.json
@@ -0,0 +1,20 @@
+{
+ "vertex": "simpleclouds:clouds_transparency",
+ "fragment": "simpleclouds:clouds_transparency",
+ "attributes": [
+ "Position"
+ ],
+ "samplers": [
+ { "name": "BayerMatrixSampler" }
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ { "name": "DarknessColorModifier", "type": "float", "count": 3, "values": [ 0.0, 0.0, 0.15 ] },
+ { "name": "FogStart", "type": "float", "count": 1, "values": [ 1.0 ] },
+ { "name": "FogEnd", "type": "float", "count": 1, "values": [ 2.0 ] },
+ { "name": "FogColor", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ { "name": "DitherScale", "type": "float", "count": 1, "values": [ 0.05 ] }
+ ]
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/core/storm_fog_shadow_map.json b/upstream_sc/assets/simpleclouds/shaders/core/storm_fog_shadow_map.json
new file mode 100644
index 00000000..68994ed3
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/core/storm_fog_shadow_map.json
@@ -0,0 +1,16 @@
+{
+ "vertex": "simpleclouds:clouds_shadow_map",
+ "fragment": "simpleclouds:storm_fog_shadow_map",
+ "attributes": [
+ "Position"
+ ],
+ "samplers": [
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ { "name": "ColorThreshold", "type": "float", "count": 3, "values": [ 0.7, 0.7, 0.7 ] },
+ { "name": "HeightCutoff", "type": "float", "count": 1, "values": [ 32.0 ] }
+ ]
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/post/final_composite.json b/upstream_sc/assets/simpleclouds/shaders/post/final_composite.json
new file mode 100644
index 00000000..bfdef362
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/post/final_composite.json
@@ -0,0 +1,17 @@
+{
+ "targets": [
+ "swap"
+ ],
+ "passes": [
+ {
+ "name": "simpleclouds:clouds_composite",
+ "intarget": "minecraft:main",
+ "outtarget": "swap"
+ },
+ {
+ "name": "blit",
+ "intarget": "swap",
+ "outtarget": "minecraft:main"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/post/final_composite_no_transparency.json b/upstream_sc/assets/simpleclouds/shaders/post/final_composite_no_transparency.json
new file mode 100644
index 00000000..382b13de
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/post/final_composite_no_transparency.json
@@ -0,0 +1,17 @@
+{
+ "targets": [
+ "output"
+ ],
+ "passes": [
+ {
+ "name": "simpleclouds:clouds_composite_no_transparency",
+ "intarget": "minecraft:main",
+ "outtarget": "output"
+ },
+ {
+ "name": "blit",
+ "intarget": "output",
+ "outtarget": "minecraft:main"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/upstream_sc/assets/simpleclouds/shaders/post/storm_post.json b/upstream_sc/assets/simpleclouds/shaders/post/storm_post.json
new file mode 100644
index 00000000..bfb49981
--- /dev/null
+++ b/upstream_sc/assets/simpleclouds/shaders/post/storm_post.json
@@ -0,0 +1,17 @@
+{
+ "targets": [
+ "out"
+ ],
+ "passes": [
+ {
+ "name": "simpleclouds:storm_fog",
+ "intarget": "minecraft:main",
+ "outtarget": "out"
+ },
+ {
+ "name": "blit",
+ "intarget": "out",
+ "outtarget": "minecraft:main"
+ }
+ ]
+}
From 3e91a875dde99848e36377f984efdb4feed06e4c Mon Sep 17 00:00:00 2001
From: Gaboouu <99047760+xGabou@users.noreply.github.com>
Date: Fri, 24 Apr 2026 15:28:10 -0400
Subject: [PATCH 05/23] feat: implement hurricane cloud rendering and
diagnostics, enhance shader support and volume management
---
.idea/workspace.xml | 160 ++---
CHANGES.md | 20 +-
build.gradle | 2 +-
.../client/ClientTickHandler.java | 6 +
.../client/render/HurricaneShaders.java | 64 ++
.../render/SimpleCloudsHurricaneRenderer.java | 582 ++++++++++++++++++
.../render/SimpleCloudsRenderDiagnostics.java | 268 +++++++-
...CloudMeshGeneratorDiagnosticsAccessor.java | 10 +
.../CloudMeshGeneratorDiagnosticsMixin.java | 112 ++++
.../mixin/CloudMeshGeneratorShaderMixin.java | 12 +-
.../MultiRegionCloudMeshGeneratorMixin.java | 106 +++-
.../client/DefaultPipelineHurricaneMixin.java | 71 +++
.../client/DefaultPipelineTornadoMixin.java | 22 +
.../DhSupportPipelineDiagnosticsMixin.java | 54 ++
.../ShaderSupportPipelineHurricaneMixin.java | 72 +++
.../ShaderSupportPipelineTornadoMixin.java | 22 +
.../SimpleCloudsRendererDhFallbackMixin.java | 45 ++
.../SimpleCloudsRendererDiagnosticsMixin.java | 42 +-
.../modules/core/CloudLibrary.java | 27 +-
.../hurricane/HurricaneCloudVolume.java | 105 ++++
.../modules/hurricane/HurricaneInstance.java | 28 +
.../modules/hurricane/HurricaneManager.java | 4 +
.../hurricane/HurricaneRenderDescriptor.java | 151 +++++
.../modules/hurricane/HurricaneSnapshot.java | 55 ++
.../modules/tornado/TornadoCommand.java | 76 ++-
.../modules/tornado/TornadoManager.java | 19 +-
.../projectatmosphere/util/WeatherType.java | 8 +-
.../shaders/core/hurricane_clouds.fsh | 284 +++++++++
.../shaders/core/hurricane_clouds.json | 57 ++
.../shaders/core/hurricane_clouds.vsh | 11 +
.../core/hurricane_clouds_transparency.fsh | 273 ++++++++
.../core/hurricane_clouds_transparency.json | 52 ++
.../shaders/core/hurricane_eye_mask.fsh | 149 +++++
.../shaders/core/hurricane_eye_mask.json | 48 ++
.../core/hurricane_eye_mask_transparency.fsh | 157 +++++
.../core/hurricane_eye_mask_transparency.json | 49 ++
.../shaders/core/hurricane_volume_box.vsh | 19 +
.../simpleclouds/cloud_spawning/pattern.json | 41 ++
.../simpleclouds/cloud_types/altocumulus.json | 42 +-
.../simpleclouds/cloud_types/altostratus.json | 32 +-
.../cloud_types/cumulus_congestus.json | 39 +-
.../cloud_types/cumulus_humilis.json | 30 +-
.../cloud_types/cumulus_mediocris.json | 40 +-
.../cloud_types/custom_cumulonimbus.json | 62 +-
.../simpleclouds/cloud_types/pattern.json | 18 +
.../cloud_types/stratocumulus_opacus.json | 58 +-
.../resources/projectatmosphere.mixins.json | 5 +
47 files changed, 3274 insertions(+), 335 deletions(-)
create mode 100644 src/main/java/net/Gabou/projectatmosphere/client/render/HurricaneShaders.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/client/render/SimpleCloudsHurricaneRenderer.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/mixin/CloudMeshGeneratorDiagnosticsMixin.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/mixin/client/DefaultPipelineHurricaneMixin.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/mixin/client/DhSupportPipelineDiagnosticsMixin.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/mixin/client/ShaderSupportPipelineHurricaneMixin.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/mixin/client/SimpleCloudsRendererDhFallbackMixin.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneCloudVolume.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneRenderDescriptor.java
create mode 100644 src/main/java/net/Gabou/projectatmosphere/modules/hurricane/HurricaneSnapshot.java
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_clouds.fsh
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_clouds.json
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_clouds.vsh
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_clouds_transparency.fsh
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_clouds_transparency.json
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_eye_mask.fsh
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_eye_mask.json
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_eye_mask_transparency.fsh
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_eye_mask_transparency.json
create mode 100644 src/main/resources/assets/projectatmosphere/shaders/core/hurricane_volume_box.vsh
create mode 100644 src/main/resources/data/simpleclouds/cloud_spawning/pattern.json
create mode 100644 src/main/resources/data/simpleclouds/cloud_types/pattern.json
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 3a028070..ca1fa473 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -7,23 +7,24 @@
-
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -142,7 +143,7 @@
],
"lastFilter": {}
}