diff --git a/build.gradle b/build.gradle index 1a915ca..146fa7f 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,8 @@ loom { sourceSet sourceSets.client } } + + accessWidenerPath = file("src/main/resources/veila.accesswidener") } fabricApi { diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogChunkProvider.java b/src/client/java/su/divan2000/veila/client/render/fog/FogChunkProvider.java index b780660..af41635 100644 --- a/src/client/java/su/divan2000/veila/client/render/fog/FogChunkProvider.java +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogChunkProvider.java @@ -1,58 +1,100 @@ package su.divan2000.veila.client.render.fog; -import java.nio.FloatBuffer; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.world.chunk.ChunkSection; +import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.chunk.PalettedContainer; +import java.util.Arrays; public final class FogChunkProvider { - static void fillChunk( - int chunkX, - int chunkZ, - FloatBuffer buffer - ) { - float density = - hash(chunkX, chunkZ); + public static boolean isChunkLoaded(int chunkX, int chunkZ) { + MinecraftClient client = MinecraftClient.getInstance(); - for (int z = 0; - z < FogWorldVolume.CHUNK_SIZE; - z++) { + if (client.world == null) { + return false; + } - for (int y = 0; - y < FogWorldVolume.SIZE_Y; - y++) { + return client.world.getChunkManager().getWorldChunk(chunkX, chunkZ, false) != null; + } - int worldY = - FogWorldVolume.MIN_Y + y; - - float value = - worldY < 70 - ? density - : 0.0f; - - for (int x = 0; - x < FogWorldVolume.CHUNK_SIZE; - x++) { - - buffer.put(value); - } - } + public static float computeVoxelValue(BlockState state) { + if (state.isOf(Blocks.POPPY)) { + return 0.2f; + } else if (state.isOf(Blocks.GRASS)) { + return 0.4f; + } else { + return 0.0f; } } - private static float hash( - int x, - int z - ) { + public static boolean fillChunkIntoArray(int chunkX, int chunkZ, float[] array) { + MinecraftClient client = MinecraftClient.getInstance(); - int h = x * 73428767; + if (!isChunkLoaded(chunkX, chunkZ)) return false; - h ^= z * 912931; + WorldChunk chunk = client.world.getChunk(chunkX, chunkZ); - h ^= h >> 13; + ChunkSection[] sections = chunk.getSectionArray(); + int bottomY = client.world.getBottomY(); + int topYExclusive = bottomY + sections.length * 16; - h *= 1274126177; + int index = 0; - return ((h >>> 24) & 255) - / 255.0f - * 0.08f; + // Порядок циклов Z -> Y -> X критичен для glTexSubImage3D + for (int z = 0; z < FogWorldVolume.CHUNK_SIZE; z++) { + // Кэшируем секции и их состояние + int currentSectionIndex = -1; + ChunkSection currentSection = null; + PalettedContainer currentContainer = null; + boolean currentIsEmpty = false; + + for (int y = 0; y < FogWorldVolume.SIZE_Y; y++) { + int worldY = FogWorldVolume.MIN_Y + y; + + // За пределами мира (например, ниже -64) + if (worldY < bottomY || worldY >= topYExclusive) { + Arrays.fill(array, index, index + FogWorldVolume.CHUNK_SIZE, 0.0f); + index += FogWorldVolume.CHUNK_SIZE; + continue; + } + + int newSectionIndex = (worldY - bottomY) >> 4; + if (newSectionIndex != currentSectionIndex) { + currentSectionIndex = newSectionIndex; + currentSection = sections[currentSectionIndex]; + if (currentSection != null) { + currentContainer = currentSection.getBlockStateContainer(); + currentIsEmpty = currentSection.isEmpty(); + } else { + currentContainer = null; + currentIsEmpty = true; + } + } + + int localY = worldY & 15; + + // ОПТИМИЗАЦИЯ: Если текущая секция полностью пустая (только воздух) + if (currentIsEmpty) { + Arrays.fill(array, index, index + FogWorldVolume.CHUNK_SIZE, 0.0f); + index += FogWorldVolume.CHUNK_SIZE; + continue; + } + + // Предварительное вычисление базового индекса для строки Y и Z + // Формула локального индекса в секции 16x16x16: (y << 8) | (z << 4) | x + int baseIndex = (localY << 8) | (z << 4); + + for (int x = 0; x < FogWorldVolume.CHUNK_SIZE; x++) { + BlockState currentBlock = currentContainer.get(baseIndex | x); + + array[index++] = computeVoxelValue(currentBlock); + } + } + } + + return true; } -} +} \ No newline at end of file diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogChunkStreamer.java b/src/client/java/su/divan2000/veila/client/render/fog/FogChunkStreamer.java index fbcbf7a..9b407c1 100644 --- a/src/client/java/su/divan2000/veila/client/render/fog/FogChunkStreamer.java +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogChunkStreamer.java @@ -1,173 +1,287 @@ package su.divan2000.veila.client.render.fog; -import java.nio.FloatBuffer; +import net.minecraft.block.BlockState; -import static su.divan2000.veila.client.render.fog.FogWorldVolume.CHUNK_SIZE; -import static su.divan2000.veila.client.render.fog.FogWorldVolume.SIZE_Y; +import java.util.concurrent.ConcurrentLinkedQueue; public final class FogChunkStreamer { + private enum ChunkStatus { + PENDING, + LOADED + } + + // Очередь изменений блоков (потокобезопасная) + private static final ConcurrentLinkedQueue blockChangeQueue = + new ConcurrentLinkedQueue<>(); + + private static final int TOTAL_CHUNKS = + FogWorldVolume.CHUNKS_X * FogWorldVolume.CHUNKS_Z; + + private static final int CHUNK_DATA_SIZE = + FogWorldVolume.CHUNK_SIZE * + FogWorldVolume.SIZE_Y * + FogWorldVolume.CHUNK_SIZE; + + private static final int MAX_CHUNKS_PER_TICK = 4; + + private static final ChunkStatus[] statuses = new ChunkStatus[TOTAL_CHUNKS]; + private static final boolean[] readyToUpload = new boolean[TOTAL_CHUNKS]; + + private static final float[][] preparedData = new float[TOTAL_CHUNKS][CHUNK_DATA_SIZE]; + private static final int[] preparedWorldChunkX = new int[TOTAL_CHUNKS]; + private static final int[] preparedWorldChunkZ = new int[TOTAL_CHUNKS]; + + private static volatile int currentOriginChunkX; + private static volatile int currentOriginChunkZ; + private static volatile int currentRingChunkOffsetX; + private static volatile int currentRingChunkOffsetZ; + + private static boolean firstUpdate = true; + private FogChunkStreamer() { } + private record BlockChangeEvent( + int chunkX, + int chunkZ, + int blockX, + int blockY, + int blockZ, + BlockState newState + ) {} + + // Вызывается из любого потока (в том числе серверного) + public static void onBlockChanged( + int chunkX, + int chunkZ, + int blockX, + int blockY, + int blockZ, + BlockState newState + ) { + blockChangeQueue.add(new BlockChangeEvent(chunkX, chunkZ, blockX, blockY, blockZ, newState)); + } + public static void update( int oldOriginChunkX, int oldOriginChunkZ, int newOriginChunkX, - int newOriginChunkZ + int newOriginChunkZ, + int ringChunkOffsetX, + int ringChunkOffsetZ ) { + currentOriginChunkX = newOriginChunkX; + currentOriginChunkZ = newOriginChunkZ; + currentRingChunkOffsetX = ringChunkOffsetX; + currentRingChunkOffsetZ = ringChunkOffsetZ; + + if (firstUpdate) { + markAllPending(newOriginChunkX, newOriginChunkZ); + firstUpdate = false; + return; + } int dx = newOriginChunkX - oldOriginChunkX; int dz = newOriginChunkZ - oldOriginChunkZ; - /* - * Телепорт. - */ - - if (Math.abs(dx) >= FogWorldVolume.CHUNKS_X || - Math.abs(dz) >= FogWorldVolume.CHUNKS_Z) { - - refillAll( - newOriginChunkX, - newOriginChunkZ - ); - + if (dx == 0 && dz == 0) { + uploadReadyData(); return; } - /* - * Новые колонки. - */ + if (Math.abs(dx) >= FogWorldVolume.CHUNKS_X || + Math.abs(dz) >= FogWorldVolume.CHUNKS_Z) { + markAllPending(newOriginChunkX, newOriginChunkZ); + uploadReadyData(); + return; + } if (dx > 0) { - for (int i = 0; i < dx; i++) { - - uploadColumn( - newOriginChunkX + - FogWorldVolume.CHUNKS_X - dx + i, - newOriginChunkZ - ); + int worldChunkX = newOriginChunkX + FogWorldVolume.CHUNKS_X - dx + i; + for (int z = 0; z < FogWorldVolume.CHUNKS_Z; z++) { + markSlotPending(worldChunkX, newOriginChunkZ + z); + } } } if (dx < 0) { - for (int i = 0; i < -dx; i++) { - - uploadColumn( - newOriginChunkX + i, - newOriginChunkZ - ); + int worldChunkX = newOriginChunkX + i; + for (int z = 0; z < FogWorldVolume.CHUNKS_Z; z++) { + markSlotPending(worldChunkX, newOriginChunkZ + z); + } } } - /* - * Новые строки. - */ - if (dz > 0) { - - for (int i = 0; i < dz; i++) { - - uploadRow( - newOriginChunkX, - newOriginChunkZ + - FogWorldVolume.CHUNKS_Z - dz + i - ); + for (int x = 0; x < FogWorldVolume.CHUNKS_X; x++) { + int worldChunkX = newOriginChunkX + x; + for (int i = 0; i < dz; i++) { + int worldChunkZ = newOriginChunkZ + FogWorldVolume.CHUNKS_Z - dz + i; + markSlotPending(worldChunkX, worldChunkZ); + } } } if (dz < 0) { - - for (int i = 0; i < -dz; i++) { - - uploadRow( - newOriginChunkX, - newOriginChunkZ + i - ); - } - } - } - - private static void refillAll( - int originChunkX, - int originChunkZ - ) { - - for (int z = 0; z < FogWorldVolume.CHUNKS_Z; z++) { - for (int x = 0; x < FogWorldVolume.CHUNKS_X; x++) { + int worldChunkX = newOriginChunkX + x; + for (int i = 0; i < -dz; i++) { + int worldChunkZ = newOriginChunkZ + i; + markSlotPending(worldChunkX, worldChunkZ); + } + } + } - uploadChunk( - originChunkX + x, - originChunkZ + z - ); + uploadReadyData(); + } + + // Обрабатывает очередь изменений блоков (только в render thread) + public static void processBlockChanges() { + BlockChangeEvent event; + while ((event = blockChangeQueue.poll()) != null) { + processSingleBlockChange(event); + } + } + + private static void processSingleBlockChange(BlockChangeEvent event) { + int originX = currentOriginChunkX; + int originZ = currentOriginChunkZ; + int ringOffsetX = currentRingChunkOffsetX; + int ringOffsetZ = currentRingChunkOffsetZ; + + // Проверяем, находится ли чанк в нашей области + int relativeX = event.chunkX - originX; + int relativeZ = event.chunkZ - originZ; + + if (relativeX < 0 || relativeX >= FogWorldVolume.CHUNKS_X || + relativeZ < 0 || relativeZ >= FogWorldVolume.CHUNKS_Z) { + return; // Чанк вне области обработки + } + + // Вычисляем физический индекс + int physicalX = Math.floorMod(relativeX + ringOffsetX, FogWorldVolume.CHUNKS_X); + int physicalZ = Math.floorMod(relativeZ + ringOffsetZ, FogWorldVolume.CHUNKS_Z); + + // Вычисляем позицию вокселя + int localX = event.blockX & 15; + int localZ = event.blockZ & 15; + int yIndex = event.blockY - FogWorldVolume.MIN_Y; + + if (yIndex < 0 || yIndex >= FogWorldVolume.SIZE_Y) { + return; // Y вне диапазона + } + + // Вычисляем значение для этого вокселя + float value = FogChunkProvider.computeVoxelValue(event.newState); + + // Обновляем один воксель в текстуре + FogWorldVolume.updateSingleVoxel( + physicalX, + physicalZ, + localX, + yIndex, + localZ, + value + ); + } + + public static void processPending() { + int originX = currentOriginChunkX; + int originZ = currentOriginChunkZ; + int ringOffsetX = currentRingChunkOffsetX; + int ringOffsetZ = currentRingChunkOffsetZ; + + int processedCount = 0; + + for (int physicalIndex = 0; physicalIndex < TOTAL_CHUNKS; physicalIndex++) { + if (statuses[physicalIndex] != ChunkStatus.PENDING) { + continue; + } + + if (processedCount >= MAX_CHUNKS_PER_TICK) { + break; + } + + int physicalX = physicalIndex % FogWorldVolume.CHUNKS_X; + int physicalZ = physicalIndex / FogWorldVolume.CHUNKS_X; + + int worldChunkX = originX + Math.floorMod( + physicalX - ringOffsetX, + FogWorldVolume.CHUNKS_X + ); + int worldChunkZ = originZ + Math.floorMod( + physicalZ - ringOffsetZ, + FogWorldVolume.CHUNKS_Z + ); + + if (FogChunkProvider.fillChunkIntoArray(worldChunkX, worldChunkZ, preparedData[physicalIndex])) { + preparedWorldChunkX[physicalIndex] = worldChunkX; + preparedWorldChunkZ[physicalIndex] = worldChunkZ; + readyToUpload[physicalIndex] = true; + processedCount++; } } } - private static void uploadColumn( - int worldChunkX, - int originChunkZ - ) { - - for (int z = 0; z < FogWorldVolume.CHUNKS_Z; z++) { - - uploadChunk( - worldChunkX, - originChunkZ + z - ); + private static void markAllPending(int originChunkX, int originChunkZ) { + for (int physicalZ = 0; physicalZ < FogWorldVolume.CHUNKS_Z; physicalZ++) { + for (int physicalX = 0; physicalX < FogWorldVolume.CHUNKS_X; physicalX++) { + int worldChunkX = originChunkX + physicalX; + int worldChunkZ = originChunkZ + physicalZ; + markSlotPending(worldChunkX, worldChunkZ); + } } } - private static void uploadRow( - int originChunkX, - int worldChunkZ - ) { + private static void markSlotPending(int worldChunkX, int worldChunkZ) { + int physicalX = Math.floorMod(worldChunkX, FogWorldVolume.CHUNKS_X); + int physicalZ = Math.floorMod(worldChunkZ, FogWorldVolume.CHUNKS_Z); + int physicalIndex = physicalZ * FogWorldVolume.CHUNKS_X + physicalX; - for (int x = 0; x < FogWorldVolume.CHUNKS_X; x++) { + FogWorldVolume.uploadZeros(physicalX, physicalZ); + statuses[physicalIndex] = ChunkStatus.PENDING; + readyToUpload[physicalIndex] = false; + } - uploadChunk( - originChunkX + x, - worldChunkZ + public static void uploadReadyData() { + int originX = currentOriginChunkX; + int originZ = currentOriginChunkZ; + int ringOffsetX = currentRingChunkOffsetX; + int ringOffsetZ = currentRingChunkOffsetZ; + + for (int physicalIndex = 0; physicalIndex < TOTAL_CHUNKS; physicalIndex++) { + if (!readyToUpload[physicalIndex]) { + continue; + } + + int physicalX = physicalIndex % FogWorldVolume.CHUNKS_X; + int physicalZ = physicalIndex / FogWorldVolume.CHUNKS_X; + + int currentWorldX = originX + Math.floorMod( + physicalX - ringOffsetX, + FogWorldVolume.CHUNKS_X ); + int currentWorldZ = originZ + Math.floorMod( + physicalZ - ringOffsetZ, + FogWorldVolume.CHUNKS_Z + ); + + if (preparedWorldChunkX[physicalIndex] == currentWorldX && + preparedWorldChunkZ[physicalIndex] == currentWorldZ) { + + FogWorldVolume.uploadChunkFromArray( + physicalX, + physicalZ, + preparedData[physicalIndex] + ); + + statuses[physicalIndex] = ChunkStatus.LOADED; + } + + readyToUpload[physicalIndex] = false; } } - - private static void uploadChunk( - int worldChunkX, - int worldChunkZ - ) { - - FloatBuffer buffer = - FogWorldVolume.chunkBuffer(); - - FogChunkProvider.fillChunk( - worldChunkX, - worldChunkZ, - buffer - ); - - buffer.flip(); - - int physicalChunkX = - Math.floorMod( - worldChunkX, - FogWorldVolume.CHUNKS_X - ); - - int physicalChunkZ = - Math.floorMod( - worldChunkZ, - FogWorldVolume.CHUNKS_Z - ); - - FogWorldVolume.uploadChunk( - physicalChunkX, - physicalChunkZ, - buffer - ); - } - } \ No newline at end of file diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogSystem.java b/src/client/java/su/divan2000/veila/client/render/fog/FogSystem.java index d70a367..eeabc4c 100644 --- a/src/client/java/su/divan2000/veila/client/render/fog/FogSystem.java +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogSystem.java @@ -2,8 +2,6 @@ package su.divan2000.veila.client.render.fog; import net.minecraft.client.MinecraftClient; -import java.nio.FloatBuffer; - public final class FogSystem { private static boolean initialized; @@ -17,12 +15,6 @@ public final class FogSystem { private FogSystem() { } - /* - * Вызывается в начале render() - * - * Только Render Thread. - */ - public static void beginRender() { if (!initialized) { @@ -30,64 +22,23 @@ public final class FogSystem { } updateWindow(); - } - /* - * Вызывается каждый client tick. - * - * Пока пусто. - */ + // Всегда копируем готовые данные в текстуру + FogChunkStreamer.uploadReadyData(); + + // Всегда обрабатываем изменения блоков + FogChunkStreamer.processBlockChanges(); + } public static void tick() { - + FogChunkStreamer.processPending(); } - /* - * Инициализация всей системы. - */ - private static void initialize() { - FogWorldVolume.init(); - initialized = true; } - /* - * Пока просто заполняем весь объём - * тестовыми чанками. - */ - - private static void fillTestVolume() { - - for (int chunkZ = 0; - chunkZ < FogWorldVolume.CHUNKS_Z; - chunkZ++) { - - for (int chunkX = 0; - chunkX < FogWorldVolume.CHUNKS_X; - chunkX++) { - - FloatBuffer buffer = - FogWorldVolume.chunkBuffer(); - - FogChunkProvider.fillChunk( - originChunkX + chunkX, - originChunkZ + chunkZ, - buffer - ); - - buffer.flip(); - - FogWorldVolume.uploadChunk( - chunkX, - chunkZ, - buffer - ); - } - } - } - private static void updateWindow() { MinecraftClient client = MinecraftClient.getInstance(); @@ -126,7 +77,9 @@ public final class FogSystem { oldOriginChunkX, oldOriginChunkZ, originChunkX, - originChunkZ + originChunkZ, + ringChunkOffsetX, + ringChunkOffsetZ ); } diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogWorldVolume.java b/src/client/java/su/divan2000/veila/client/render/fog/FogWorldVolume.java index 996f251..bcc94c1 100644 --- a/src/client/java/su/divan2000/veila/client/render/fog/FogWorldVolume.java +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogWorldVolume.java @@ -43,6 +43,11 @@ public final class FogWorldVolume { * 16 × 384 × 16 */ + private static final float[] ZEROS_ARRAY = new float[CHUNK_SIZE * SIZE_Y * CHUNK_SIZE]; + static { + java.util.Arrays.fill(ZEROS_ARRAY, 0.0f); + } + private static final FloatBuffer CHUNK_BUFFER = BufferUtils.createFloatBuffer( CHUNK_SIZE * @@ -50,6 +55,10 @@ public final class FogWorldVolume { CHUNK_SIZE ); + + private static final FloatBuffer SINGLE_VOXEL_BUFFER = + BufferUtils.createFloatBuffer(1); + private FogWorldVolume() { } @@ -93,7 +102,7 @@ public final class FogWorldVolume { GL11.glTexParameteri( GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_WRAP_S, - GL12.GL_CLAMP_TO_EDGE + GL12.GL_REPEAT ); GL11.glTexParameteri( @@ -105,7 +114,7 @@ public final class FogWorldVolume { GL11.glTexParameteri( GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_WRAP_R, - GL12.GL_CLAMP_TO_EDGE + GL12.GL_REPEAT ); GL11.glBindTexture( @@ -188,6 +197,58 @@ public final class FogWorldVolume { GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); } + public static void updateSingleVoxel( + int textureChunkX, + int textureChunkZ, + int localX, + int localY, + int localZ, + float value + ) { + GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); + + SINGLE_VOXEL_BUFFER.clear(); + SINGLE_VOXEL_BUFFER.put(value); + SINGLE_VOXEL_BUFFER.flip(); + + GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); + GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, 0); + GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, 0); + + GL30.glTexSubImage3D( + GL12.GL_TEXTURE_3D, + 0, + textureChunkX * CHUNK_SIZE + localX, + localY, + textureChunkZ * CHUNK_SIZE + localZ, + 1, 1, 1, + GL11.GL_RED, + GL11.GL_FLOAT, + SINGLE_VOXEL_BUFFER + ); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + } + + public static void uploadZeros(int textureChunkX, int textureChunkZ) { + FloatBuffer buffer = chunkBuffer(); + buffer.put(ZEROS_ARRAY); + buffer.flip(); + uploadChunk(textureChunkX, textureChunkZ, buffer); + } + + public static void uploadChunkFromArray( + int textureChunkX, + int textureChunkZ, + float[] data + ) { + FloatBuffer buffer = chunkBuffer(); + buffer.put(data); + buffer.flip(); + uploadChunk(textureChunkX, textureChunkZ, buffer); + } + public static FloatBuffer chunkBuffer() { CHUNK_BUFFER.clear(); diff --git a/src/client/java/su/divan2000/veila/mixin/client/WorldChunkMixin.java b/src/client/java/su/divan2000/veila/mixin/client/WorldChunkMixin.java new file mode 100644 index 0000000..5130978 --- /dev/null +++ b/src/client/java/su/divan2000/veila/mixin/client/WorldChunkMixin.java @@ -0,0 +1,41 @@ +package su.divan2000.veila.mixin.client; + +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.chunk.WorldChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import su.divan2000.veila.client.render.fog.FogChunkStreamer; + +@Mixin(WorldChunk.class) +public class WorldChunkMixin { + @Inject( + method = "setBlockState", + at = @At("RETURN") + ) + private void veila$onBlockChanged( + BlockPos pos, + BlockState state, + boolean moved, + CallbackInfoReturnable cir + ) { + // Только если блок реально изменился + if (cir.getReturnValue() != null) { + WorldChunk self = (WorldChunk) (Object) this; + int chunkX = self.getPos().x; + int chunkZ = self.getPos().z; + + // Уведомляем стример об изменении + FogChunkStreamer.onBlockChanged( + chunkX, + chunkZ, + pos.getX(), + pos.getY(), + pos.getZ(), + state + ); + } + } +} \ No newline at end of file diff --git a/src/client/resources/assets/veila/shaders/red.frag b/src/client/resources/assets/veila/shaders/red.frag index d6e707e..ecddfd7 100644 --- a/src/client/resources/assets/veila/shaders/red.frag +++ b/src/client/resources/assets/veila/shaders/red.frag @@ -20,18 +20,27 @@ out vec4 FragColor; // Настройки //------------------------------------------------------------ -const float STEP_SIZE = 0.5; -const float MAX_DISTANCE = 256.0; -const int MAX_STEPS = int(MAX_DISTANCE / STEP_SIZE); - -// Размер туманного объёма const float FOG_SIZE_X = 256.0; -const float FOG_SIZE_Z = 256.0; +const float FOG_SIZE_Z = FOG_SIZE_X; + +const int MAX_STEPS = 256; // Уменьшили количество шагов благодаря адаптивности // Высота мира const float WORLD_BOTTOM = -64.0; const float WORLD_HEIGHT = 384.0; +const float MAX_DISTANCE = (FOG_SIZE_X / 2.0) - 18.0; + +// Коэффициент нелинейности распределения шагов: +// 1.0 = равномерные шаги (как было) +// 1.5 = мягкое увеличение (рекомендуется для начала) +// 2.0 = линейный рост размера шага с расстоянием (хороший баланс) +// 2.5 = более агрессивное увеличение (лучшая производительность) +const float GAMMA = 2.0; + +// Включение jittering для устранения banding артефактов +const bool USE_JITTERING = true; + //------------------------------------------------------------ vec3 reconstructViewPosition(vec2 uv) @@ -68,23 +77,19 @@ float density(vec3 worldPos) float localZ = worldPos.z - float(FogOrigin.y); if ( - localX < 0.0 || localX >= FOG_SIZE_X || - localZ < 0.0 || localZ >= FOG_SIZE_Z || - worldPos.y < WORLD_BOTTOM || - worldPos.y >= WORLD_BOTTOM + WORLD_HEIGHT + localX < 0.0 || localX >= FOG_SIZE_X || + localZ < 0.0 || localZ >= FOG_SIZE_Z || + worldPos.y < WORLD_BOTTOM || + worldPos.y >= WORLD_BOTTOM + WORLD_HEIGHT ){ return 0.0; } - float physicalX = mod( - localX + float(RingBlockOffset.x), - FOG_SIZE_X - ); + float physicalX = + localX + float(RingBlockOffset.x); - float physicalZ = mod( - localZ + float(RingBlockOffset.y), - FOG_SIZE_Z - ); + float physicalZ = + localZ + float(RingBlockOffset.y); uv.x = physicalX / FOG_SIZE_X; uv.z = physicalZ / FOG_SIZE_Z; @@ -95,6 +100,21 @@ float density(vec3 worldPos) //------------------------------------------------------------ +// Псевдослучайное значение для jittering (детерминированное по UV) +float random(vec2 st) +{ + return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123); +} + +// Функция нелинейного маппинга параметра t [0,1] в расстояние +// Использует степенную функцию для распределения шагов +float mapParameterToDistance(float t) +{ + return MAX_DISTANCE * pow(t, GAMMA); +} + +//------------------------------------------------------------ + void main() { vec4 sceneColor = texture(DiffuseSampler, texCoord); @@ -109,17 +129,52 @@ void main() vec3 samplePos = CameraPosition; float fog = 0.0; + float currentDistance = 0.0; - for (int i = 0; i < MAX_STEPS; i++) - { - float traveled = float(i) * STEP_SIZE; + // Jittering начальной позиции для устранения banding (ступенчатых артефактов) + // Это стандартная техника в volumetric rendering + float jitter = 0.0; + if (USE_JITTERING) { + // Вычисляем размер первого шага для корректного jittering + float firstStep = mapParameterToDistance(1.0 / float(MAX_STEPS)); + jitter = random(texCoord) * firstStep; + currentDistance = jitter; + samplePos += rayDir * currentDistance; + } - if (traveled >= rayLength) - break; + for (int i = 0; i < MAX_STEPS; i++) { + // Параметр t меняется от 0 до 1 + float t = float(i + 1) / float(MAX_STEPS); - samplePos += rayDir * STEP_SIZE; + // Вычисляем следующее расстояние через нелинейное маппинг + float nextDistance = mapParameterToDistance(t); - fog += density(samplePos) * STEP_SIZE; + // Ограничиваем максимальной длиной луча + if (nextDistance > rayLength) { + nextDistance = rayLength; + } + + // Размер текущего шага + float stepSize = nextDistance - currentDistance; + + // Проверяем, не вышли ли за пределы луча + if (stepSize <= 0.0) { + break; + } + + // Продвигаемся по лучу + samplePos += rayDir * stepSize; + + // Интегрируем плотность (Beer-Lambert law approximation) + // Важно умножать на stepSize для корректной физической модели + fog += density(samplePos) * stepSize; + + currentDistance = nextDistance; + + // Прерываем, если достигли конца луча + if (currentDistance >= rayLength) { + break; + } } fog = clamp(fog, 0.0, 1.0); diff --git a/src/client/resources/veila.client.mixins.json b/src/client/resources/veila.client.mixins.json index 76fa6b9..e3e8d7b 100644 --- a/src/client/resources/veila.client.mixins.json +++ b/src/client/resources/veila.client.mixins.json @@ -5,7 +5,8 @@ "compatibilityLevel": "JAVA_17", "client": [ "GameRendererMixin", - "MinecraftClientMixin" + "MinecraftClientMixin", + "WorldChunkMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 1107183..cf63573 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -2,6 +2,7 @@ "schemaVersion": 1, "id": "veila", "version": "${version}", + "accessWidener": "veila.accesswidener", "name": "Veila", "description": "", diff --git a/src/main/resources/veila.accesswidener b/src/main/resources/veila.accesswidener new file mode 100644 index 0000000..8d1dbf8 --- /dev/null +++ b/src/main/resources/veila.accesswidener @@ -0,0 +1,2 @@ +accessWidener v2 named +accessible method net/minecraft/world/chunk/PalettedContainer get (I)Ljava/lang/Object; \ No newline at end of file