diff --git a/src/client/java/su/divan2000/veila/client/render/PostProcessingManager.java b/src/client/java/su/divan2000/veila/client/render/PostProcessingManager.java index 605647d..001aa04 100644 --- a/src/client/java/su/divan2000/veila/client/render/PostProcessingManager.java +++ b/src/client/java/su/divan2000/veila/client/render/PostProcessingManager.java @@ -5,6 +5,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gl.Framebuffer; import net.minecraft.client.render.Camera; import net.minecraft.util.math.Vec3d; +import net.minecraft.world.LightType; import org.joml.Matrix4f; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.*; @@ -13,6 +14,7 @@ import su.divan2000.veila.client.gl.FullscreenQuad; import su.divan2000.veila.client.gl.GLProgram; import su.divan2000.veila.client.gl.GLShader; import su.divan2000.veila.client.gl.ResourceUtil; +import su.divan2000.veila.client.render.fog.FogLightVolume; import su.divan2000.veila.client.render.fog.FogSimulationManager; import su.divan2000.veila.client.render.fog.FogSystem; import su.divan2000.veila.client.render.fog.FogWorldVolume; @@ -62,15 +64,28 @@ public class PostProcessingManager { private static int inverseViewLocation; private static int fogVolumeLocation; - private static int fogOriginLocation; + private static int lightVolumeLocation; + private static int fogOriginLocation; private static int ringOffsetLocation; + private static float fogR, fogG, fogB; + private static int fogColorLocation; + private static int skyBrightnessLocation; + + public static void setFogColor(float r, float g, float b) { + fogR = r; + fogG = g; + fogB = b; + } + public static void init() { if (program != null) return; + setFogColor(0.75f, 0.80f, 0.90f); + GLShader vertex = new GLShader( GL20.GL_VERTEX_SHADER, ResourceUtil.load("shaders/fullscreen.vert") @@ -78,7 +93,7 @@ public class PostProcessingManager { GLShader fragment = new GLShader( GL20.GL_FRAGMENT_SHADER, - ResourceUtil.load("shaders/red.frag") + ResourceUtil.load("shaders/fog_raymarching.frag") ); program = new GLProgram(vertex, fragment); @@ -90,6 +105,8 @@ public class PostProcessingManager { depthSamplerLocation = program.uniform("DepthSampler"); fogVolumeLocation = program.uniform("FogVolume"); + lightVolumeLocation = program.uniform("LightVolume"); + fogOriginLocation = program.uniform("FogOrigin"); ringOffsetLocation = program.uniform("RingBlockOffset"); @@ -99,6 +116,9 @@ public class PostProcessingManager { viewLocation = program.uniform("View"); inverseViewLocation = program.uniform("InverseView"); + fogColorLocation = program.uniform("FogColor"); + skyBrightnessLocation = program.uniform("SkyBrightness"); + FullscreenQuad.init(); vertex.delete(); @@ -107,13 +127,16 @@ public class PostProcessingManager { System.out.println("VEILA shader compiled!"); } - public static void render() { + public static void render(float tickDelta) { if (program == null) return; MinecraftClient client = MinecraftClient.getInstance(); + if (client.world == null) + return; + Framebuffer framebuffer = client.getFramebuffer(); int width = framebuffer.textureWidth; @@ -163,6 +186,13 @@ public class PostProcessingManager { 2 ); + GL13.glActiveTexture(GL13.GL_TEXTURE3); + GL11.glBindTexture( + GL12.GL_TEXTURE_3D, + FogLightVolume.getTexture() + ); + GL20.glUniform1i(lightVolumeLocation, 3); + GL20.glUniform2i( fogOriginLocation, FogSystem.originChunkX() * FogWorldVolume.CHUNK_SIZE, @@ -229,8 +259,31 @@ public class PostProcessingManager { height ); + /* + * Цвет тумана + */ + + GL20.glUniform3f( + fogColorLocation, + fogR, + fogG, + fogB + ); + + /* + * Яркость тумана + */ + + GL20.glUniform1f( + skyBrightnessLocation, + client.world.getSkyBrightness(tickDelta)*0.75f+0.25f + ); + FullscreenQuad.draw(); + GL13.glActiveTexture(GL13.GL_TEXTURE3); + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + GL13.glActiveTexture(GL13.GL_TEXTURE2); GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogBlockRegistry.java b/src/client/java/su/divan2000/veila/client/render/fog/FogBlockRegistry.java index b533bbf..867c83e 100644 --- a/src/client/java/su/divan2000/veila/client/render/fog/FogBlockRegistry.java +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogBlockRegistry.java @@ -21,7 +21,7 @@ public final class FogBlockRegistry { BLOCK_CACHE_INITIALIZED = new boolean[blockCount]; // Waterlogged блок всегда содержит воду - WATER_VALUE = new FogBlockProperties(true, 1, 60).pack(); + WATER_VALUE = new FogBlockProperties(true, 1, 20).pack(); } private FogBlockRegistry() { @@ -54,10 +54,10 @@ public final class FogBlockRegistry { // Источники тумана (sign = +1) // WATER уже обработан в getPackedProperties if (block == Blocks.GRASS || block == Blocks.TALL_GRASS || block == Blocks.FERN) { - return new FogBlockProperties(solid, 1, 10).pack(); + return new FogBlockProperties(solid, 1, 50).pack(); } if (block == Blocks.VINE) { - return new FogBlockProperties(solid, 1, 5).pack(); + return new FogBlockProperties(solid, 1, 45).pack(); } // Поглотители тумана (sign = -1) diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogLightProvider.java b/src/client/java/su/divan2000/veila/client/render/fog/FogLightProvider.java new file mode 100644 index 0000000..7e4c391 --- /dev/null +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogLightProvider.java @@ -0,0 +1,71 @@ +package su.divan2000.veila.client.render.fog; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkSectionPos; +import net.minecraft.world.LightType; +import net.minecraft.world.chunk.ChunkNibbleArray; +import net.minecraft.world.chunk.light.ChunkLightProvider; +import net.minecraft.world.chunk.light.LightingProvider; + +public final class FogLightProvider { + + public static final int SECTION_DATA_SIZE = 4096; + + private FogLightProvider() { + } + + public static byte[] fillSectionIntoArray(int sectionX, int sectionY, int sectionZ) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world == null) return null; + + LightingProvider provider = client.world.getLightingProvider(); + + // Прямой доступ к провайдерам через AccessWidener + ChunkLightProvider blockProvider = provider.blockLightProvider; + ChunkLightProvider skyProvider = provider.skyLightProvider; + + // Проверка на null (поля @Nullable) + if (blockProvider == null || skyProvider == null) { + return null; + } + + // Используем ПУБЛИЧНЫЙ метод ChunkLightProvider.getLightSection() + ChunkSectionPos sectionPos = ChunkSectionPos.from(sectionX, sectionY, sectionZ); + + ChunkNibbleArray blockArray = blockProvider.getLightSection(sectionPos); + ChunkNibbleArray skyArray = skyProvider.getLightSection(sectionPos); + + if (blockArray == null && skyArray == null) { + return null; + } + + byte[] result = new byte[SECTION_DATA_SIZE]; + int index = 0; + + // Порядок заполнения: Z → Y → X (как в FogChunkProvider) + for (int localZ = 0; localZ < 16; localZ++) { + for (int localY = 0; localY < 16; localY++) { + for (int localX = 0; localX < 16; localX++) { + int blockLight = (blockArray != null) ? blockArray.get(localX, localY, localZ) : 0; + int skyLight = (skyArray != null) ? skyArray.get(localX, localY, localZ) : 0; + + result[index++] = FogLightVolume.packLight(blockLight, skyLight); + } + } + } + + return result; + } + + public static byte computeLightValue(BlockPos pos) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world == null) return FogLightVolume.DEFAULT_LIGHT; + + LightingProvider provider = client.world.getLightingProvider(); + int blockLight = provider.get(LightType.BLOCK).getLightLevel(pos); + int skyLight = provider.get(LightType.SKY).getLightLevel(pos); + + return FogLightVolume.packLight(blockLight, skyLight); + } +} \ No newline at end of file diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogLightStreamer.java b/src/client/java/su/divan2000/veila/client/render/fog/FogLightStreamer.java new file mode 100644 index 0000000..a6e8aca --- /dev/null +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogLightStreamer.java @@ -0,0 +1,361 @@ +package su.divan2000.veila.client.render.fog; + +import java.util.concurrent.atomic.AtomicLongArray; + +public final class FogLightStreamer { + + public static final int SECTION_SIZE = 16; + public static final int SECTIONS_PER_CHUNK_Y = FogLightVolume.SIZE_Y / SECTION_SIZE; + + // Общее количество секций в окне: 16 чанков × 16 чанков × 24 секции = 6144 + private static final int TOTAL_SECTIONS = + FogLightVolume.CHUNKS_X * FogLightVolume.CHUNKS_Z * SECTIONS_PER_CHUNK_Y; + + // Битовая маска: 6144 бита = 96 long = 768 байт + private static final AtomicLongArray pendingSectionsMask = + new AtomicLongArray((TOTAL_SECTIONS + 63) / 64); + + private static final AtomicLongArray readySectionsMask = + new AtomicLongArray((TOTAL_SECTIONS + 63) / 64); + + private static final AtomicLongArray initialLoadMask = + new AtomicLongArray((TOTAL_SECTIONS + 63) / 64); + + private static final int MAX_SECTIONS_PER_FRAME = 64; + private static final int MAX_INITIAL_SECTIONS_PER_TICK = 96; + + 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 static final int MAX_REGIONS = 16; + private static final int[] regionPhysX = new int[MAX_REGIONS]; + private static final int[] regionPhysZ = new int[MAX_REGIONS]; + private static final int[] regionWidth = new int[MAX_REGIONS]; + private static final int[] regionDepth = new int[MAX_REGIONS]; + private static int regionCount = 0; + + private FogLightStreamer() { + } + + /** + * Конвертирует мировые координаты секции в локальный индекс в маске. + * Возвращает -1 если секция вне текущего окна. + */ + private static int sectionToLocalIndex(int sectionX, int sectionY, int sectionZ) { + int chunkX = sectionX; + int chunkZ = sectionZ; + + int relativeX = chunkX - currentOriginChunkX; + int relativeZ = chunkZ - currentOriginChunkZ; + + if (relativeX < 0 || relativeX >= FogLightVolume.CHUNKS_X || + relativeZ < 0 || relativeZ >= FogLightVolume.CHUNKS_Z) { + return -1; + } + + int localSectionY = sectionY - (FogWorldVolume.MIN_Y >> 4); + if (localSectionY < 0 || localSectionY >= SECTIONS_PER_CHUNK_Y) { + return -1; + } + + int physicalX = Math.floorMod(relativeX + currentRingChunkOffsetX, FogLightVolume.CHUNKS_X); + int physicalZ = Math.floorMod(relativeZ + currentRingChunkOffsetZ, FogLightVolume.CHUNKS_Z); + + return (localSectionY * FogLightVolume.CHUNKS_Z * FogLightVolume.CHUNKS_X) + + (physicalZ * FogLightVolume.CHUNKS_X) + + physicalX; + } + + /** + * Устанавливает бит в маске атомарно. + */ + private static boolean setBit(AtomicLongArray mask, int index) { + int wordIndex = index >> 6; + long bitMask = 1L << (index & 63); + + while (true) { + long current = mask.get(wordIndex); + if ((current & bitMask) != 0) { + return false; // Бит уже установлен + } + if (mask.compareAndSet(wordIndex, current, current | bitMask)) { + return true; + } + // CAS не удался - повторяем + } + } + + /** + * Снимает бит из маски атомарно. + */ + private static boolean clearBit(AtomicLongArray mask, int index) { + int wordIndex = index >> 6; + long bitMask = 1L << (index & 63); + + while (true) { + long current = mask.get(wordIndex); + if ((current & bitMask) == 0) { + return false; // Бит уже снят + } + if (mask.compareAndSet(wordIndex, current, current & ~bitMask)) { + return true; + } + } + } + + /** + * Проверяет и снимает бит (для итерации). + * Возвращает индекс если бит был установлен. + */ + private static int nextSetBit(AtomicLongArray mask, int startIndex) { + for (int i = startIndex; i < TOTAL_SECTIONS; i++) { + int wordIndex = i >> 6; + long bitMask = 1L << (i & 63); + + if ((mask.get(wordIndex) & bitMask) != 0) { + return i; + } + } + return -1; + } + + public static void onSectionUpdated(long sectionPos) { + int sectionX = net.minecraft.util.math.ChunkSectionPos.unpackX(sectionPos); + int sectionY = net.minecraft.util.math.ChunkSectionPos.unpackY(sectionPos); + int sectionZ = net.minecraft.util.math.ChunkSectionPos.unpackZ(sectionPos); + + int index = sectionToLocalIndex(sectionX, sectionY, sectionZ); + if (index >= 0) { + setBit(pendingSectionsMask, index); + } + } + + public static void update( + int oldOriginChunkX, + int oldOriginChunkZ, + int newOriginChunkX, + int newOriginChunkZ, + int ringChunkOffsetX, + int ringChunkOffsetZ + ) { + currentOriginChunkX = newOriginChunkX; + currentOriginChunkZ = newOriginChunkZ; + currentRingChunkOffsetX = ringChunkOffsetX; + currentRingChunkOffsetZ = ringChunkOffsetZ; + + if (firstUpdate) { + markAllPending(); + firstUpdate = false; + return; + } + + int dx = newOriginChunkX - oldOriginChunkX; + int dz = newOriginChunkZ - oldOriginChunkZ; + + if (dx == 0 && dz == 0) { + return; + } + + if (Math.abs(dx) >= FogLightVolume.CHUNKS_X || + Math.abs(dz) >= FogLightVolume.CHUNKS_Z) { + markAllPending(); + return; + } + + regionCount = 0; + + if (dx > 0) { + int worldX = newOriginChunkX + FogLightVolume.CHUNKS_X - dx; + int worldZ = newOriginChunkZ; + collectRegions(worldX, worldZ, dx, FogLightVolume.CHUNKS_Z); + addInitialLoadForRange(worldX, worldZ, dx, FogLightVolume.CHUNKS_Z); + } + + if (dx < 0) { + int worldX = newOriginChunkX; + int worldZ = newOriginChunkZ; + collectRegions(worldX, worldZ, -dx, FogLightVolume.CHUNKS_Z); + addInitialLoadForRange(worldX, worldZ, -dx, FogLightVolume.CHUNKS_Z); + } + + if (dz > 0) { + int worldX = newOriginChunkX; + int worldZ = newOriginChunkZ + FogLightVolume.CHUNKS_Z - dz; + collectRegions(worldX, worldZ, FogLightVolume.CHUNKS_X, dz); + addInitialLoadForRange(worldX, worldZ, FogLightVolume.CHUNKS_X, dz); + } + + if (dz < 0) { + int worldX = newOriginChunkX; + int worldZ = newOriginChunkZ; + collectRegions(worldX, worldZ, FogLightVolume.CHUNKS_X, -dz); + addInitialLoadForRange(worldX, worldZ, FogLightVolume.CHUNKS_X, -dz); + } + + if (regionCount > 0) { + FogLightVolume.clearRegions(regionPhysX, regionPhysZ, regionWidth, regionDepth, regionCount); + } + } + + private static void addInitialLoadForRange(int worldX, int worldZ, int countX, int countZ) { + for (int i = 0; i < countX; i++) { + for (int j = 0; j < countZ; j++) { + int chunkX = worldX + i; + int chunkZ = worldZ + j; + + for (int sectionY = 0; sectionY < SECTIONS_PER_CHUNK_Y; sectionY++) { + int sectionPos = sectionToLocalIndex( + chunkX, + sectionY + (FogWorldVolume.MIN_Y >> 4), + chunkZ + ); + if (sectionPos >= 0) { + setBit(initialLoadMask, sectionPos); + } + } + } + } + } + + private static void markAllPending() { + regionCount = 1; + regionPhysX[0] = 0; + regionPhysZ[0] = 0; + regionWidth[0] = FogLightVolume.CHUNKS_X; + regionDepth[0] = FogLightVolume.CHUNKS_Z; + + FogLightVolume.clearRegions(regionPhysX, regionPhysZ, regionWidth, regionDepth, regionCount); + + // Устанавливаем все биты в initialLoadMask + for (int i = 0; i < pendingSectionsMask.length(); i++) { + initialLoadMask.set(i, -1L); // Все биты = 1 + } + // Очищаем лишние биты в последнем слове + int totalWords = (TOTAL_SECTIONS + 63) / 64; + int extraBits = TOTAL_SECTIONS & 63; + if (extraBits != 0) { + initialLoadMask.set(totalWords - 1, (1L << extraBits) - 1); + } + } + + private static void collectRegions(int worldX, int worldZ, int countX, int countZ) { + int[] xStarts = new int[2]; + int[] xCounts = new int[2]; + int xSegCount = splitIntoSegments(worldX, countX, FogLightVolume.CHUNKS_X, xStarts, xCounts); + + int[] zStarts = new int[2]; + int[] zCounts = new int[2]; + int zSegCount = splitIntoSegments(worldZ, countZ, FogLightVolume.CHUNKS_Z, zStarts, zCounts); + + for (int i = 0; i < xSegCount; i++) { + for (int j = 0; j < zSegCount; j++) { + if (regionCount >= MAX_REGIONS) return; + regionPhysX[regionCount] = xStarts[i]; + regionPhysZ[regionCount] = zStarts[j]; + regionWidth[regionCount] = xCounts[i]; + regionDepth[regionCount] = zCounts[j]; + regionCount++; + } + } + } + + private static int splitIntoSegments( + int start, int count, int dim, + int[] outStarts, int[] outCounts + ) { + if (count == 0) return 0; + + int first = Math.floorMod(start, dim); + int last = Math.floorMod(start + count - 1, dim); + + if (first <= last) { + outStarts[0] = first; + outCounts[0] = count; + return 1; + } else { + int firstCount = dim - first; + outStarts[0] = first; + outCounts[0] = firstCount; + outStarts[1] = 0; + outCounts[1] = count - firstCount; + return 2; + } + } + + public static void prepareInitialLoads() { + int processed = 0; + int index = 0; + + while (processed < MAX_INITIAL_SECTIONS_PER_TICK) { + index = nextSetBit(initialLoadMask, index); + if (index < 0) break; + + // Конвертируем индекс обратно в координаты секции + int sectionX = currentOriginChunkX + (index % FogLightVolume.CHUNKS_X); + int sectionZ = currentOriginChunkZ + ((index / FogLightVolume.CHUNKS_X) % FogLightVolume.CHUNKS_Z); + int sectionY = (index / (FogLightVolume.CHUNKS_X * FogLightVolume.CHUNKS_Z)) + (FogWorldVolume.MIN_Y >> 4); + + byte[] sectionData = FogLightProvider.fillSectionIntoArray(sectionX, sectionY, sectionZ); + if (sectionData != null) { + clearBit(initialLoadMask, index); + setBit(readySectionsMask, index); + processed++; + } + index++; + } + } + + public static void uploadReadySections() { + int processed = 0; + int index = 0; + + while (processed < MAX_SECTIONS_PER_FRAME) { + index = nextSetBit(readySectionsMask, index); + if (index < 0) break; + + if (uploadSectionByIndex(index)) { + clearBit(readySectionsMask, index); + processed++; + } + index++; + } + } + + public static void processSectionUpdates() { + int processed = 0; + int index = 0; + + while (processed < MAX_SECTIONS_PER_FRAME) { + index = nextSetBit(pendingSectionsMask, index); + if (index < 0) break; + + if (uploadSectionByIndex(index)) { + clearBit(pendingSectionsMask, index); + processed++; + } + index++; + } + } + + private static boolean uploadSectionByIndex(int index) { + int physicalX = index % FogLightVolume.CHUNKS_X; + int physicalZ = (index / FogLightVolume.CHUNKS_X) % FogLightVolume.CHUNKS_Z; + int localSectionY = index / (FogLightVolume.CHUNKS_X * FogLightVolume.CHUNKS_Z); + + int sectionX = currentOriginChunkX + Math.floorMod(physicalX - currentRingChunkOffsetX, FogLightVolume.CHUNKS_X); + int sectionZ = currentOriginChunkZ + Math.floorMod(physicalZ - currentRingChunkOffsetZ, FogLightVolume.CHUNKS_Z); + int sectionY = localSectionY + (FogWorldVolume.MIN_Y >> 4); + + byte[] sectionData = FogLightProvider.fillSectionIntoArray(sectionX, sectionY, sectionZ); + if (sectionData == null) { + return false; + } + + FogLightVolume.uploadSection(physicalX, localSectionY, physicalZ, sectionData); + return true; + } +} \ No newline at end of file diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogLightVolume.java b/src/client/java/su/divan2000/veila/client/render/fog/FogLightVolume.java new file mode 100644 index 0000000..13e1b78 --- /dev/null +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogLightVolume.java @@ -0,0 +1,284 @@ +package su.divan2000.veila.client.render.fog; + +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL31; + +import java.nio.ByteBuffer; + +import static su.divan2000.veila.client.render.fog.FogLightStreamer.SECTION_SIZE; + +public final class FogLightVolume { + + public static final int SIZE_X = FogWorldVolume.SIZE_X; + public static final int SIZE_Z = FogWorldVolume.SIZE_Z; + public static final int SIZE_Y = FogWorldVolume.SIZE_Y; + public static final int CHUNK_SIZE = FogWorldVolume.CHUNK_SIZE; + public static final int CHUNKS_X = FogWorldVolume.CHUNKS_X; + public static final int CHUNKS_Z = FogWorldVolume.CHUNKS_Z; + + private static int texture = -1; + + // Упаковка: bits 0-3 = BlockLight, bits 4-7 = SkyLight + public static final byte DEFAULT_LIGHT = packLight(0, 15); // Темно, полное небо + + private static final byte[] ZEROS_ARRAY; + static { + ZEROS_ARRAY = new byte[CHUNK_SIZE * SIZE_Y * CHUNK_SIZE]; + java.util.Arrays.fill(ZEROS_ARRAY, DEFAULT_LIGHT); + } + + private static final ByteBuffer CHUNK_BUFFER = + BufferUtils.createByteBuffer(CHUNK_SIZE * SIZE_Y * CHUNK_SIZE); + + private static final ByteBuffer SINGLE_VOXEL_BUFFER = + BufferUtils.createByteBuffer(1); + + // PBO для быстрой очистки (как в FogWorldVolume) + private static int clearPBO = -1; + private static final int MAX_CLEAR_SIZE = SIZE_X * SIZE_Y * SIZE_Z; + + private FogLightVolume() { + } + + public static byte packLight(int blockLight, int skyLight) { + return (byte) (((skyLight & 0xF) << 4) | (blockLight & 0xF)); + } + + public static int unpackBlockLight(byte packed) { + return packed & 0xF; + } + + public static int unpackSkyLight(byte packed) { + return (packed >> 4) & 0xF; + } + + private static void initClearPBO() { + if (clearPBO != -1) return; + + clearPBO = GL15.glGenBuffers(); + GL15.glBindBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, clearPBO); + + GL15.glBufferData(GL31.GL_PIXEL_UNPACK_BUFFER, MAX_CLEAR_SIZE, GL15.GL_STATIC_DRAW); + + ByteBuffer mapped = GL15.glMapBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, GL15.GL_WRITE_ONLY); + if (mapped != null) { + for (int i = 0; i < MAX_CLEAR_SIZE; i++) { + mapped.put(DEFAULT_LIGHT); + } + GL15.glUnmapBuffer(GL31.GL_PIXEL_UNPACK_BUFFER); + } + + GL15.glBindBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, 0); + } + + public static void init() { + if (texture != -1) return; + + initClearPBO(); + + texture = GL11.glGenTextures(); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); + + GL30.glTexImage3D( + GL12.GL_TEXTURE_3D, + 0, + GL30.GL_R8UI, + SIZE_X, + SIZE_Y, + SIZE_Z, + 0, + GL30.GL_RED_INTEGER, + GL11.GL_UNSIGNED_BYTE, + (ByteBuffer) null + ); + + GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + + GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_WRAP_S, GL12.GL_REPEAT); + GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); + GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_WRAP_R, GL12.GL_REPEAT); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + + clear(); + } + + public static void clear() { + CHUNK_BUFFER.clear(); + CHUNK_BUFFER.put(ZEROS_ARRAY); + CHUNK_BUFFER.flip(); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); + + for (int chunkZ = 0; chunkZ < CHUNKS_Z; chunkZ++) { + for (int chunkX = 0; chunkX < CHUNKS_X; chunkX++) { + uploadChunk(chunkX, chunkZ, CHUNK_BUFFER); + CHUNK_BUFFER.rewind(); + } + } + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + } + + public static void uploadChunk(int textureChunkX, int textureChunkZ, ByteBuffer data) { + GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); + + 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, + 0, + textureChunkZ * CHUNK_SIZE, + CHUNK_SIZE, + SIZE_Y, + CHUNK_SIZE, + GL30.GL_RED_INTEGER, + GL11.GL_UNSIGNED_BYTE, + data + ); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + } + + public static void updateSingleVoxel( + int textureChunkX, + int textureChunkZ, + int localX, + int localY, + int localZ, + byte 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, + GL30.GL_RED_INTEGER, + GL11.GL_UNSIGNED_BYTE, + SINGLE_VOXEL_BUFFER + ); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + } + + public static void uploadChunkFromArray( + int textureChunkX, + int textureChunkZ, + byte[] data + ) { + ByteBuffer buffer = chunkBuffer(); + buffer.put(data); + buffer.flip(); + uploadChunk(textureChunkX, textureChunkZ, buffer); + } + + public static ByteBuffer chunkBuffer() { + CHUNK_BUFFER.clear(); + return CHUNK_BUFFER; + } + + public static int getTexture() { + return texture; + } + + public static void clearRegions( + int[] physX, int[] physZ, + int[] widthChunks, int[] depthChunks, + int count + ) { + GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); + GL15.glBindBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, clearPBO); + + 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); + + for (int i = 0; i < count; i++) { + int w = widthChunks[i] * CHUNK_SIZE; + int h = SIZE_Y; + int d = depthChunks[i] * CHUNK_SIZE; + + GL30.glTexSubImage3D( + GL12.GL_TEXTURE_3D, + 0, + physX[i] * CHUNK_SIZE, + 0, + physZ[i] * CHUNK_SIZE, + w, h, d, + GL30.GL_RED_INTEGER, + GL11.GL_UNSIGNED_BYTE, + 0L + ); + } + + GL15.glBindBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, 0); + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + } + + // Статический буфер для одной секции (4096 байт) + private static final ByteBuffer SECTION_BUFFER = + BufferUtils.createByteBuffer(SECTION_SIZE * SECTION_SIZE * SECTION_SIZE); + + /** + * Загружает одну секцию (16×16×16) в указанную позицию текстуры. + * textureSectionX/Z - в чанках (0-15), textureSectionY - индекс секции по Y (0-23). + */ + public static void uploadSection( + int textureSectionX, + int textureSectionY, + int textureSectionZ, + byte[] data + ) { + SECTION_BUFFER.clear(); + SECTION_BUFFER.put(data); + SECTION_BUFFER.flip(); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); + + 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, + textureSectionX * CHUNK_SIZE, + textureSectionY * SECTION_SIZE, + textureSectionZ * CHUNK_SIZE, + SECTION_SIZE, + SECTION_SIZE, + SECTION_SIZE, + GL30.GL_RED_INTEGER, + GL11.GL_UNSIGNED_BYTE, + SECTION_BUFFER + ); + + GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); + } +} \ No newline at end of file diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationManager.java b/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationManager.java index 49de4f1..5a4e0ac 100644 --- a/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationManager.java +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationManager.java @@ -8,12 +8,12 @@ public final class FogSimulationManager { // Симулируем каждые 100 мс (10 раз в секунду) private static final long SIMULATION_INTERVAL_NS = 100_000_000L; - private static float globalEmissionMultiplier = 1.0f; + private static float globalEmissionMultiplier = 0.6f; private static float globalAbsorptionMultiplier = 1.0f; - private static float globalDecay = 0.01f; - private static float diffusionRate = 0.1f; - private static float upDiffusion = 0.9f; - private static float downDiffusion = 1.1f; + private static float globalDecay = 0.025f; + private static float diffusionRate = 0.15f; + private static float upDiffusion = 1.5f; + private static float downDiffusion = 1.0f; private static float horizontalDiffusion = 1.0f; private FogSimulationManager() { diff --git a/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationShader.java b/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationShader.java index 0b299ab..db52487 100644 --- a/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationShader.java +++ b/src/client/java/su/divan2000/veila/client/render/fog/FogSimulationShader.java @@ -182,7 +182,7 @@ public final class FogSimulationShader { z ); - GL20.glUniform1f(currentZLocation, (float) z / FogWorldVolume.SIZE_Z); + GL20.glUniform1f(currentZLocation, (float) (z + 0.5f) / FogWorldVolume.SIZE_Z); FullscreenQuad.draw(); } 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 18a1891..ef061ac 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 @@ -23,21 +23,30 @@ public final class FogSystem { updateWindow(); - // Всегда копируем готовые данные в текстуру + // Блоки и density: загружаем готовые данные FogChunkStreamer.uploadReadyData(); - // Всегда обрабатываем изменения блоков + // Блоки и density: обрабатываем изменения FogChunkStreamer.processBlockChanges(); + // Освещение: загружаем готовые секции + инкрементальные обновления (render thread) + FogLightStreamer.uploadReadySections(); + FogLightStreamer.processSectionUpdates(); + FogSimulationManager.update(); } public static void tick() { + // Блоки: initial load в client thread FogChunkStreamer.processPending(); + + // Освещение: читаем данные секций (client thread, без OpenGL) + FogLightStreamer.prepareInitialLoads(); } private static void initialize() { FogWorldVolume.init(); + FogLightVolume.init(); FogSimulationManager.init(); initialized = true; } @@ -84,6 +93,16 @@ public final class FogSystem { ringChunkOffsetX, ringChunkOffsetZ ); + + // Освещение: помечаем новые секции для initial load + FogLightStreamer.update( + oldOriginChunkX, + oldOriginChunkZ, + originChunkX, + originChunkZ, + ringChunkOffsetX, + ringChunkOffsetZ + ); } public static int originChunkX() { diff --git a/src/client/java/su/divan2000/veila/mixin/client/GameRendererMixin.java b/src/client/java/su/divan2000/veila/mixin/client/GameRendererMixin.java index 15536a4..8e2acd2 100644 --- a/src/client/java/su/divan2000/veila/mixin/client/GameRendererMixin.java +++ b/src/client/java/su/divan2000/veila/mixin/client/GameRendererMixin.java @@ -27,7 +27,7 @@ public class GameRendererMixin { CallbackInfo ci ) { PostProcessingManager.init(); - PostProcessingManager.render(); + PostProcessingManager.render(tickDelta); } @Inject( diff --git a/src/client/java/su/divan2000/veila/mixin/client/LightStorageMixin.java b/src/client/java/su/divan2000/veila/mixin/client/LightStorageMixin.java new file mode 100644 index 0000000..3ffe507 --- /dev/null +++ b/src/client/java/su/divan2000/veila/mixin/client/LightStorageMixin.java @@ -0,0 +1,37 @@ +package su.divan2000.veila.mixin.client; + +import net.minecraft.util.math.ChunkSectionPos; +import net.minecraft.world.chunk.ChunkNibbleArray; +import net.minecraft.world.chunk.light.LightStorage; +import org.jetbrains.annotations.Nullable; +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; +import su.divan2000.veila.client.render.fog.FogLightStreamer; + +@Mixin(LightStorage.class) +public class LightStorageMixin { + + // Ловим инкрементальные обновления (когда блок меняет свет) + @Inject( + method = "set(JI)V", + at = @At("TAIL") + ) + private void veila$onLightSet(long blockPos, int value, CallbackInfo ci) { + long sectionPos = ChunkSectionPos.fromBlockPos(blockPos); + FogLightStreamer.onSectionUpdated(sectionPos); + } + + // Ловим загрузку секций из сохранения + @Inject( + method = "enqueueSectionData(JLnet/minecraft/world/chunk/ChunkNibbleArray;)V", + at = @At("TAIL") + ) + private void veila$onEnqueueSection(long sectionPos, @Nullable ChunkNibbleArray array, CallbackInfo ci) { + // Если array == null - секция выгружается, не нужно загружать + if (array != null) { + FogLightStreamer.onSectionUpdated(sectionPos); + } + } +} \ No newline at end of file diff --git a/src/client/resources/assets/veila/shaders/red.frag b/src/client/resources/assets/veila/shaders/fog_raymarching.frag similarity index 97% rename from src/client/resources/assets/veila/shaders/red.frag rename to src/client/resources/assets/veila/shaders/fog_raymarching.frag index 28ffca7..19068ca 100644 --- a/src/client/resources/assets/veila/shaders/red.frag +++ b/src/client/resources/assets/veila/shaders/fog_raymarching.frag @@ -7,8 +7,11 @@ uniform sampler3D FogVolume; uniform mat4 InverseProjection; uniform mat4 InverseView; +uniform vec3 FogColor; uniform vec3 CameraPosition; +uniform float SkyBrightness; + uniform ivec2 FogOrigin; uniform ivec2 RingBlockOffset; @@ -179,16 +182,10 @@ void main() } } - fog = clamp(fog, 0.0, 1.0); - - vec3 fogColor = vec3( - 0.75, - 0.80, - 0.90 - ); + fog = clamp(fog, 0.0, 0.95); FragColor = vec4( - mix(sceneColor.rgb, fogColor, fog), + mix(sceneColor.rgb, FogColor*SkyBrightness, fog), 1.0 ); } \ No newline at end of file diff --git a/src/client/resources/veila.client.mixins.json b/src/client/resources/veila.client.mixins.json index e3e8d7b..dcc7ee4 100644 --- a/src/client/resources/veila.client.mixins.json +++ b/src/client/resources/veila.client.mixins.json @@ -6,7 +6,8 @@ "client": [ "GameRendererMixin", "MinecraftClientMixin", - "WorldChunkMixin" + "WorldChunkMixin", + "LightStorageMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/resources/veila.accesswidener b/src/main/resources/veila.accesswidener index 8d1dbf8..3f6ae50 100644 --- a/src/main/resources/veila.accesswidener +++ b/src/main/resources/veila.accesswidener @@ -1,2 +1,4 @@ -accessWidener v2 named -accessible method net/minecraft/world/chunk/PalettedContainer get (I)Ljava/lang/Object; \ No newline at end of file +accessWidener v2 named +accessible method net/minecraft/world/chunk/PalettedContainer get (I)Ljava/lang/Object; +accessible field net/minecraft/world/chunk/light/LightingProvider blockLightProvider Lnet/minecraft/world/chunk/light/ChunkLightProvider; +accessible field net/minecraft/world/chunk/light/LightingProvider skyLightProvider Lnet/minecraft/world/chunk/light/ChunkLightProvider;