Освещённость вокселей учитывается

This commit is contained in:
2026-08-04 20:15:35 +04:00
parent 8c1809a556
commit 76370f71e2
4 changed files with 157 additions and 151 deletions
@@ -276,7 +276,7 @@ public class PostProcessingManager {
GL20.glUniform1f( GL20.glUniform1f(
skyBrightnessLocation, skyBrightnessLocation,
client.world.getSkyBrightness(tickDelta)*0.75f+0.25f client.world.getSkyBrightness(tickDelta)
); );
FullscreenQuad.draw(); FullscreenQuad.draw();
@@ -10,7 +10,8 @@ import net.minecraft.world.chunk.light.LightingProvider;
public final class FogLightProvider { public final class FogLightProvider {
public static final int SECTION_DATA_SIZE = 4096; // Размер массива для одной секции: 16×16×16×2 = 8192 (×2 для двух каналов)
public static final int SECTION_DATA_SIZE = 16 * 16 * 16 * 2;
private FogLightProvider() { private FogLightProvider() {
} }
@@ -21,16 +22,13 @@ public final class FogLightProvider {
LightingProvider provider = client.world.getLightingProvider(); LightingProvider provider = client.world.getLightingProvider();
// Прямой доступ к провайдерам через AccessWidener
ChunkLightProvider<?, ?> blockProvider = provider.blockLightProvider; ChunkLightProvider<?, ?> blockProvider = provider.blockLightProvider;
ChunkLightProvider<?, ?> skyProvider = provider.skyLightProvider; ChunkLightProvider<?, ?> skyProvider = provider.skyLightProvider;
// Проверка на null (поля @Nullable)
if (blockProvider == null || skyProvider == null) { if (blockProvider == null || skyProvider == null) {
return null; return null;
} }
// Используем ПУБЛИЧНЫЙ метод ChunkLightProvider.getLightSection()
ChunkSectionPos sectionPos = ChunkSectionPos.from(sectionX, sectionY, sectionZ); ChunkSectionPos sectionPos = ChunkSectionPos.from(sectionX, sectionY, sectionZ);
ChunkNibbleArray blockArray = blockProvider.getLightSection(sectionPos); ChunkNibbleArray blockArray = blockProvider.getLightSection(sectionPos);
@@ -43,29 +41,19 @@ public final class FogLightProvider {
byte[] result = new byte[SECTION_DATA_SIZE]; byte[] result = new byte[SECTION_DATA_SIZE];
int index = 0; int index = 0;
// Порядок заполнения: Z → Y → X (как в FogChunkProvider)
for (int localZ = 0; localZ < 16; localZ++) { for (int localZ = 0; localZ < 16; localZ++) {
for (int localY = 0; localY < 16; localY++) { for (int localY = 0; localY < 16; localY++) {
for (int localX = 0; localX < 16; localX++) { for (int localX = 0; localX < 16; localX++) {
int blockLight = (blockArray != null) ? blockArray.get(localX, localY, localZ) : 0; int blockLight = (blockArray != null) ? blockArray.get(localX, localY, localZ) : 0;
int skyLight = (skyArray != null) ? skyArray.get(localX, localY, localZ) : 0; int skyLight = (skyArray != null) ? skyArray.get(localX, localY, localZ) : 0;
result[index++] = FogLightVolume.packLight(blockLight, skyLight); // Упаковка в два байта: R=blockLight, G=skyLight
FogLightVolume.packLight(blockLight, skyLight, result, index);
index += 2;
} }
} }
} }
return result; 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);
}
} }
@@ -9,8 +9,6 @@ import org.lwjgl.opengl.GL31;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import static su.divan2000.veila.client.render.fog.FogLightStreamer.SECTION_SIZE;
public final class FogLightVolume { public final class FogLightVolume {
public static final int SIZE_X = FogWorldVolume.SIZE_X; public static final int SIZE_X = FogWorldVolume.SIZE_X;
@@ -22,38 +20,40 @@ public final class FogLightVolume {
private static int texture = -1; private static int texture = -1;
// Упаковка: bits 0-3 = BlockLight, bits 4-7 = SkyLight // Дефолтное освещение: blockLight=0, skyLight=15 (ночь, открытое небо)
public static final byte DEFAULT_LIGHT = packLight(0, 15); // Темно, полное небо // Теперь это vec2, упакованный в RG8
public static final byte DEFAULT_BLOCK_LIGHT = 0;
public static final byte DEFAULT_SKY_LIGHT = 15;
private static final byte[] ZEROS_ARRAY; private static final byte[] DEFAULT_ARRAY;
static { static {
ZEROS_ARRAY = new byte[CHUNK_SIZE * SIZE_Y * CHUNK_SIZE]; DEFAULT_ARRAY = new byte[CHUNK_SIZE * SIZE_Y * CHUNK_SIZE * 2]; // ×2 для двух каналов
java.util.Arrays.fill(ZEROS_ARRAY, DEFAULT_LIGHT); for (int i = 0; i < CHUNK_SIZE * SIZE_Y * CHUNK_SIZE; i++) {
DEFAULT_ARRAY[i * 2] = DEFAULT_BLOCK_LIGHT; // R = blockLight
DEFAULT_ARRAY[i * 2 + 1] = DEFAULT_SKY_LIGHT; // G = skyLight
}
} }
private static final ByteBuffer CHUNK_BUFFER = private static final ByteBuffer CHUNK_BUFFER =
BufferUtils.createByteBuffer(CHUNK_SIZE * SIZE_Y * CHUNK_SIZE); BufferUtils.createByteBuffer(CHUNK_SIZE * SIZE_Y * CHUNK_SIZE * 2);
private static final ByteBuffer SINGLE_VOXEL_BUFFER = private static final ByteBuffer SINGLE_VOXEL_BUFFER =
BufferUtils.createByteBuffer(1); BufferUtils.createByteBuffer(2);
// PBO для быстрой очистки (как в FogWorldVolume) // PBO для быстрой очистки
private static int clearPBO = -1; private static int clearPBO = -1;
private static final int MAX_CLEAR_SIZE = SIZE_X * SIZE_Y * SIZE_Z; private static final int MAX_CLEAR_SIZE = SIZE_X * SIZE_Y * SIZE_Z * 2; // ×2 для двух каналов
private FogLightVolume() { private FogLightVolume() {
} }
public static byte packLight(int blockLight, int skyLight) { /**
return (byte) (((skyLight & 0xF) << 4) | (blockLight & 0xF)); * Упаковывает blockLight и skyLight в два байта для RG8 текстуры.
} * blockLight → R канал, skyLight → G канал.
*/
public static int unpackBlockLight(byte packed) { public static void packLight(int blockLight, int skyLight, byte[] out, int offset) {
return packed & 0xF; out[offset] = (byte) (blockLight * 17);
} out[offset + 1] = (byte) (skyLight * 17);
public static int unpackSkyLight(byte packed) {
return (packed >> 4) & 0xF;
} }
private static void initClearPBO() { private static void initClearPBO() {
@@ -66,8 +66,9 @@ public final class FogLightVolume {
ByteBuffer mapped = GL15.glMapBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, GL15.GL_WRITE_ONLY); ByteBuffer mapped = GL15.glMapBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, GL15.GL_WRITE_ONLY);
if (mapped != null) { if (mapped != null) {
for (int i = 0; i < MAX_CLEAR_SIZE; i++) { for (int i = 0; i < SIZE_X * SIZE_Y * SIZE_Z; i++) {
mapped.put(DEFAULT_LIGHT); mapped.put(DEFAULT_BLOCK_LIGHT);
mapped.put(DEFAULT_SKY_LIGHT);
} }
GL15.glUnmapBuffer(GL31.GL_PIXEL_UNPACK_BUFFER); GL15.glUnmapBuffer(GL31.GL_PIXEL_UNPACK_BUFFER);
} }
@@ -84,21 +85,23 @@ public final class FogLightVolume {
GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture);
// ИСПРАВЛЕНО: GL_RG8 вместо GL_R8UI, два канала для двух типов света
GL30.glTexImage3D( GL30.glTexImage3D(
GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_3D,
0, 0,
GL30.GL_R8UI, GL30.GL_RG8,
SIZE_X, SIZE_X,
SIZE_Y, SIZE_Y,
SIZE_Z, SIZE_Z,
0, 0,
GL30.GL_RED_INTEGER, GL30.GL_RG,
GL11.GL_UNSIGNED_BYTE, GL11.GL_UNSIGNED_BYTE,
(ByteBuffer) null (ByteBuffer) null
); );
GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); // GL_LINEAR для плавной интерполяции
GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_WRAP_S, GL12.GL_REPEAT); 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_T, GL12.GL_CLAMP_TO_EDGE);
@@ -111,7 +114,7 @@ public final class FogLightVolume {
public static void clear() { public static void clear() {
CHUNK_BUFFER.clear(); CHUNK_BUFFER.clear();
CHUNK_BUFFER.put(ZEROS_ARRAY); CHUNK_BUFFER.put(DEFAULT_ARRAY);
CHUNK_BUFFER.flip(); CHUNK_BUFFER.flip();
GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture);
@@ -143,7 +146,7 @@ public final class FogLightVolume {
CHUNK_SIZE, CHUNK_SIZE,
SIZE_Y, SIZE_Y,
CHUNK_SIZE, CHUNK_SIZE,
GL30.GL_RED_INTEGER, GL30.GL_RG, // ИСПРАВЛЕНО: GL_RG вместо GL_RED_INTEGER
GL11.GL_UNSIGNED_BYTE, GL11.GL_UNSIGNED_BYTE,
data data
); );
@@ -151,19 +154,17 @@ public final class FogLightVolume {
GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0);
} }
public static void updateSingleVoxel( public static void uploadSection(
int textureChunkX, int textureSectionX,
int textureChunkZ, int textureSectionY,
int localX, int textureSectionZ,
int localY, byte[] data
int localZ,
byte value
) { ) {
GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture); ByteBuffer sectionBuffer = BufferUtils.createByteBuffer(data.length);
sectionBuffer.put(data);
sectionBuffer.flip();
SINGLE_VOXEL_BUFFER.clear(); GL11.glBindTexture(GL12.GL_TEXTURE_3D, texture);
SINGLE_VOXEL_BUFFER.put(value);
SINGLE_VOXEL_BUFFER.flip();
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0); GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0);
@@ -173,13 +174,15 @@ public final class FogLightVolume {
GL30.glTexSubImage3D( GL30.glTexSubImage3D(
GL12.GL_TEXTURE_3D, GL12.GL_TEXTURE_3D,
0, 0,
textureChunkX * CHUNK_SIZE + localX, textureSectionX * CHUNK_SIZE,
localY, textureSectionY * 16,
textureChunkZ * CHUNK_SIZE + localZ, textureSectionZ * CHUNK_SIZE,
1, 1, 1, 16,
GL30.GL_RED_INTEGER, 16,
16,
GL30.GL_RG,
GL11.GL_UNSIGNED_BYTE, GL11.GL_UNSIGNED_BYTE,
SINGLE_VOXEL_BUFFER sectionBuffer
); );
GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0); GL11.glBindTexture(GL12.GL_TEXTURE_3D, 0);
@@ -230,7 +233,7 @@ public final class FogLightVolume {
0, 0,
physZ[i] * CHUNK_SIZE, physZ[i] * CHUNK_SIZE,
w, h, d, w, h, d,
GL30.GL_RED_INTEGER, GL30.GL_RG,
GL11.GL_UNSIGNED_BYTE, GL11.GL_UNSIGNED_BYTE,
0L 0L
); );
@@ -239,46 +242,4 @@ public final class FogLightVolume {
GL15.glBindBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, 0); GL15.glBindBuffer(GL31.GL_PIXEL_UNPACK_BUFFER, 0);
GL11.glBindTexture(GL12.GL_TEXTURE_3D, 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);
}
} }
@@ -3,6 +3,7 @@
uniform sampler2D DiffuseSampler; uniform sampler2D DiffuseSampler;
uniform sampler2D DepthSampler; uniform sampler2D DepthSampler;
uniform sampler3D FogVolume; uniform sampler3D FogVolume;
uniform sampler3D LightVolume;
uniform mat4 InverseProjection; uniform mat4 InverseProjection;
uniform mat4 InverseView; uniform mat4 InverseView;
@@ -28,22 +29,33 @@ const float FOG_SIZE_Z = FOG_SIZE_X;
const int MAX_STEPS = 256; const int MAX_STEPS = 256;
// Высота мира
const float WORLD_BOTTOM = -64.0; const float WORLD_BOTTOM = -64.0;
const float WORLD_HEIGHT = 384.0; const float WORLD_HEIGHT = 384.0;
const float MAX_DISTANCE = (FOG_SIZE_X / 2.0) - 18.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; const float GAMMA = 2.0;
// Включение jittering для устранения banding артефактов
const bool USE_JITTERING = false; const bool USE_JITTERING = false;
// Физические коэффициенты volumetric rendering
// EXTINCTION: насколько сильно туман поглощает/рассеивает свет (0.1-2.0)
// Больше значение = туман сильнее затеняет далёкие объекты
const float EXTINCTION_COEFF = 1.0;
// SCATTERING: насколько сильно туман сам светится от источников
// Обычно равен EXTINCTION (консервативная среда), но можно варьировать
const float SCATTERING_COEFF = 1.0;
// Ambient — базовое освещение среды (воздух, микрочастицы).
// Не зависит от времени суток и от skyLight/blockLight.
// Даёт минимальную видимость тумана даже в полной темноте.
const float AMBIENT_INTENSITY = 0.10;
// Порог transmittance для early termination (оптимизация)
// Если transmittance упал ниже этого значения, дальнейшие шаги ничего не внесут
const float MIN_TRANSMITTANCE = 0.01;
//------------------------------------------------------------ //------------------------------------------------------------
vec3 reconstructViewPosition(vec2 uv) vec3 reconstructViewPosition(vec2 uv)
@@ -57,25 +69,20 @@ vec3 reconstructViewPosition(vec2 uv)
); );
vec4 view = InverseProjection * clip; vec4 view = InverseProjection * clip;
return view.xyz / view.w; return view.xyz / view.w;
} }
vec3 reconstructWorldPosition(vec2 uv) vec3 reconstructWorldPosition(vec2 uv)
{ {
vec3 viewPos = reconstructViewPosition(uv); vec3 viewPos = reconstructViewPosition(uv);
vec4 world = InverseView * vec4(viewPos, 1.0); vec4 world = InverseView * vec4(viewPos, 1.0);
return world.xyz + CameraPosition; return world.xyz + CameraPosition;
} }
//------------------------------------------------------------ //------------------------------------------------------------
float density(vec3 worldPos) vec3 worldToUV(vec3 worldPos)
{ {
vec3 uv;
float localX = worldPos.x - float(FogOrigin.x); float localX = worldPos.x - float(FogOrigin.x);
float localZ = worldPos.z - float(FogOrigin.y); float localZ = worldPos.z - float(FogOrigin.y);
@@ -85,25 +92,43 @@ float density(vec3 worldPos)
worldPos.y < WORLD_BOTTOM || worldPos.y < WORLD_BOTTOM ||
worldPos.y >= WORLD_BOTTOM + WORLD_HEIGHT worldPos.y >= WORLD_BOTTOM + WORLD_HEIGHT
){ ){
return 0.0; return vec3(-1.0);
} }
float physicalX = float physicalX = localX + float(RingBlockOffset.x);
localX + float(RingBlockOffset.x); float physicalZ = localZ + float(RingBlockOffset.y);
float physicalZ =
localZ + float(RingBlockOffset.y);
vec3 uv;
uv.x = physicalX / FOG_SIZE_X; uv.x = physicalX / FOG_SIZE_X;
uv.z = physicalZ / FOG_SIZE_Z; uv.z = physicalZ / FOG_SIZE_Z;
uv.y = (worldPos.y - WORLD_BOTTOM) / WORLD_HEIGHT; uv.y = (worldPos.y - WORLD_BOTTOM) / WORLD_HEIGHT;
return uv;
}
float density(vec3 worldPos)
{
vec3 uv = worldToUV(worldPos);
if (uv.x < 0.0) return 0.0;
return texture(FogVolume, uv).r; return texture(FogVolume, uv).r;
} }
vec2 light(vec3 worldPos)
{
vec3 uv = worldToUV(worldPos);
if (uv.x < 0.0) return vec2(0.0);
vec2 rg = texture(LightVolume, uv).rg;
float blockNorm = rg.r;
float skyNorm = rg.g;
float skyContribution = skyNorm * SkyBrightness;
return vec2(skyContribution, blockNorm);
}
//------------------------------------------------------------ //------------------------------------------------------------
// Псевдослучайное значение для jittering (детерминированное по UV)
float random(vec2 st) float random(vec2 st)
{ {
vec3 p3 = fract(vec3(st.xyx) * 0.1031); vec3 p3 = fract(vec3(st.xyx) * 0.1031);
@@ -111,8 +136,6 @@ float random(vec2 st)
return fract((p3.x + p3.y) * p3.z); return fract((p3.x + p3.y) * p3.z);
} }
// Функция нелинейного маппинга параметра t [0,1] в расстояние
// Использует степенную функцию для распределения шагов
float mapParameterToDistance(float t) float mapParameterToDistance(float t)
{ {
return MAX_DISTANCE * pow(t, GAMMA); return MAX_DISTANCE * pow(t, GAMMA);
@@ -125,22 +148,26 @@ void main()
vec4 sceneColor = texture(DiffuseSampler, texCoord); vec4 sceneColor = texture(DiffuseSampler, texCoord);
vec3 worldPos = reconstructWorldPosition(texCoord); vec3 worldPos = reconstructWorldPosition(texCoord);
vec3 rayDir = normalize(worldPos - CameraPosition); vec3 rayDir = normalize(worldPos - CameraPosition);
float rayLength = distance(worldPos, CameraPosition); float rayLength = distance(worldPos, CameraPosition);
rayLength = min(rayLength, MAX_DISTANCE); rayLength = min(rayLength, MAX_DISTANCE);
vec3 samplePos = CameraPosition; vec3 samplePos = CameraPosition;
float fog = 0.0;
float currentDistance = 0.0; float currentDistance = 0.0;
// Jittering начальной позиции для устранения banding (ступенчатых артефактов) // Transmittance: сколько света от сцены дошло до текущего шага
// Это стандартная техника в volumetric rendering // Начинается с 1.0 (всё доходит) и экспоненциально падает
float transmittance = 1.0;
// Накопленный in-scattered свет (взвешенный по transmittance)
float skyScattered = 0.0;
float blockScattered = 0.0;
float ambientScattered = 0.0;
float jitter = 0.0; float jitter = 0.0;
if (USE_JITTERING) { if (USE_JITTERING) {
// Вычисляем размер первого шага для корректного jittering
float firstStep = mapParameterToDistance(1.0 / float(MAX_STEPS)); float firstStep = mapParameterToDistance(1.0 / float(MAX_STEPS));
jitter = random(texCoord) * firstStep; jitter = random(texCoord) * firstStep;
currentDistance = jitter; currentDistance = jitter;
@@ -148,44 +175,74 @@ void main()
} }
for (int i = 0; i < MAX_STEPS; i++) { for (int i = 0; i < MAX_STEPS; i++) {
// Параметр t меняется от 0 до 1
float t = float(i + 1) / float(MAX_STEPS); float t = float(i + 1) / float(MAX_STEPS);
// Вычисляем следующее расстояние через нелинейное маппинг
float nextDistance = mapParameterToDistance(t); float nextDistance = mapParameterToDistance(t);
// Ограничиваем максимальной длиной луча
if (nextDistance > rayLength) { if (nextDistance > rayLength) {
nextDistance = rayLength; nextDistance = rayLength;
} }
// Размер текущего шага
float stepSize = nextDistance - currentDistance; float stepSize = nextDistance - currentDistance;
// Проверяем, не вышли ли за пределы луча
if (stepSize <= 0.0) { if (stepSize <= 0.0) {
break; break;
} }
// Продвигаемся по лучу
samplePos += rayDir * stepSize; samplePos += rayDir * stepSize;
// Интегрируем плотность (Beer-Lambert law approximation) float d = density(samplePos);
// Важно умножать на stepSize для корректной физической модели
fog += density(samplePos) * stepSize; // Оптическая толщина этого шага
float opticalDepth = d * stepSize * EXTINCTION_COEFF;
if (d > 0.0) {
// Получаем освещение в текущей точке
vec2 lightValues = light(samplePos);
float totalLightAtPoint = lightValues.x + lightValues.y;
// Базовая формула in-scattering для каждого источника
float baseScattering = d * stepSize * SCATTERING_COEFF;
// In-scattering: свет, рассеянный в камерy из этого шага.
// Умножается на transmittance до этого шага (дальний туман затенён ближним).
// Умножается на плотность (больше частиц = больше рассеяния).
float stepScattering = totalLightAtPoint * d * stepSize * SCATTERING_COEFF;
// Вклад ослабляется текущим transmittance
skyScattered += lightValues.x * baseScattering * transmittance;
blockScattered += lightValues.y * baseScattering * transmittance;
ambientScattered += AMBIENT_INTENSITY * baseScattering * transmittance;
// Обновляем transmittance: Beer-Lambert law
transmittance *= exp(-opticalDepth);
}
currentDistance = nextDistance; currentDistance = nextDistance;
// Прерываем, если достигли конца луча // Early termination: если transmittance упал почти до нуля,
// дальние шаги ничего не внесут в итоговый цвет
if (transmittance < MIN_TRANSMITTANCE) {
transmittance = 0.0;
break;
}
if (currentDistance >= rayLength) { if (currentDistance >= rayLength) {
break; break;
} }
} }
fog = clamp(fog, 0.0, 0.95); // Суммарное освещение, рассеянное в камеру
float totalScattered = skyScattered + blockScattered + ambientScattered;
FragColor = vec4( // Финальный цвет:
mix(sceneColor.rgb, FogColor*SkyBrightness, fog), // - sceneColor × transmittance: сцена, ослабленная туманом (Beer-Lambert)
1.0 // - FogColor × totalScattered: свет, который рассеялся в тумане и попал в камеру
); //
// Эта формула физически корректна:
// - Когда тумана нет (transmittance=1, scattered=0): result = sceneColor ✓
// - Когда туман очень плотный (transmittance=0): result = FogColor × scattered ✓
// - Фонарь в толще тумана: ближние слои дают scattered, дальние уже не видны
vec3 finalColor = sceneColor.rgb * transmittance + FogColor * totalScattered;
FragColor = vec4(finalColor, 1.0);
} }