Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8551b5c28 | |||
| f443f0a94a | |||
| e33daac636 | |||
| 661c852bbf | |||
| 428713d4bc |
@@ -22,7 +22,39 @@ jobs:
|
||||
if: inputs.os == 'macos' || inputs.os == 'both'
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Проверка релиза
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -e
|
||||
if ! [[ "$TAG" =~ ^build-[0-9]+$ ]]; then echo "::error::тег '$TAG' не похож на build-N"; exit 1; fi
|
||||
REL_JSON=$(mktemp)
|
||||
CODE=$(curl -s -o "$REL_JSON" -w '%{http_code}' -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG")
|
||||
if [ "$CODE" != "200" ]; then echo "::error::релиза с тегом $TAG нет (HTTP $CODE): сначала дождись сборки build.yml на main"; exit 1; fi
|
||||
python3 - "$TAG" "$REL_JSON" <<'PY'
|
||||
import json, os, sys
|
||||
tag, path = sys.argv[1], sys.argv[2]
|
||||
r = json.load(open(path))
|
||||
assets = [a.get('name') for a in (r.get('assets') or [])]
|
||||
if r.get('tag_name') != tag or r.get('name') != tag or 'airdef.hmod' not in assets:
|
||||
print('::error::релиз %s не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем' % tag)
|
||||
sys.exit(1)
|
||||
with open(os.environ['GITHUB_ENV'], 'a') as f:
|
||||
f.write('RELEASE_ID=%s\nRELEASE_SHA=%s\n' % (r['id'], r.get('target_commitish', '')))
|
||||
PY
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: refs/tags/${{ inputs.tag }}
|
||||
- name: Проверка коммита
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -e
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
if [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] && [ "$HEAD_SHA" != "$RELEASE_SHA" ]; then echo "::error::тег $TAG указывает на $HEAD_SHA, а релиз создан из $RELEASE_SHA"; exit 1; fi
|
||||
echo "собираю $TAG = $HEAD_SHA"
|
||||
- name: Rust
|
||||
run: |
|
||||
command -v cargo || curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||
@@ -47,11 +79,10 @@ jobs:
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -e
|
||||
ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef-macos-arm64.tar.gz" "$API/releases/$ID/assets?name=airdef-macos-arm64.tar.gz" > /dev/null
|
||||
# RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml)
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef-macos-arm64.tar.gz" "$API/releases/$RELEASE_ID/assets?name=airdef-macos-arm64.tar.gz" > /dev/null
|
||||
|
||||
game-windows:
|
||||
if: inputs.os == 'windows' || inputs.os == 'both'
|
||||
@@ -60,7 +91,34 @@ jobs:
|
||||
run:
|
||||
shell: pwsh
|
||||
steps:
|
||||
- name: Проверка релиза
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:TAG -notmatch '^build-\d+$') { Write-Output "::error::тег '$env:TAG' не похож на build-N"; exit 1 }
|
||||
$h = @{ Authorization = "token $env:TOKEN" }
|
||||
try { $r = Invoke-RestMethod -Headers $h "$env:API/releases/tags/$env:TAG" }
|
||||
catch { Write-Output "::error::релиза с тегом $env:TAG нет: сначала дождись сборки build.yml на main"; exit 1 }
|
||||
$assets = @($r.assets | ForEach-Object { $_.name })
|
||||
if ($r.tag_name -ne $env:TAG -or $r.name -ne $env:TAG -or ($assets -notcontains 'airdef.hmod')) {
|
||||
Write-Output "::error::релиз $env:TAG не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем"; exit 1
|
||||
}
|
||||
"RELEASE_ID=$($r.id)" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV
|
||||
"RELEASE_SHA=$($r.target_commitish)" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: refs/tags/${{ inputs.tag }}
|
||||
- name: Проверка коммита
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$head = git rev-parse HEAD
|
||||
if ($env:RELEASE_SHA -match '^[0-9a-f]{40}$' -and $head -ne $env:RELEASE_SHA) { Write-Output "::error::тег $env:TAG указывает на $head, а релиз создан из $env:RELEASE_SHA"; exit 1 }
|
||||
Write-Output "собираю $env:TAG = $head"
|
||||
- name: Rust
|
||||
run: |
|
||||
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
|
||||
@@ -88,9 +146,7 @@ jobs:
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
$h = @{ Authorization = "token $env:TOKEN" }
|
||||
$id = (Invoke-RestMethod -Headers $h "$env:API/releases/tags/$env:TAG").id
|
||||
curl.exe -sf -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@out\airdef-windows-x86_64.zip" "$env:API/releases/$id/assets?name=airdef-windows-x86_64.zip"
|
||||
# RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml)
|
||||
curl.exe -sf -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@out\airdef-windows-x86_64.zip" "$env:API/releases/$env:RELEASE_ID/assets?name=airdef-windows-x86_64.zip"
|
||||
|
||||
|
||||
@@ -25,7 +25,39 @@ jobs:
|
||||
FREE=$(df --output=avail -BG / | tail -1 | tr -dc 0-9)
|
||||
echo "свободно: ${FREE} ГБ"
|
||||
if [ "$FREE" -lt 15 ]; then echo "::error::на сервере свободно ${FREE} ГБ, нужно хотя бы 15: сборка игры забьёт диск и уронит Gitea"; exit 1; fi
|
||||
- name: Проверка релиза
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -e
|
||||
if ! [[ "$TAG" =~ ^build-[0-9]+$ ]]; then echo "::error::тег '$TAG' не похож на build-N"; exit 1; fi
|
||||
REL_JSON=$(mktemp)
|
||||
CODE=$(curl -s -o "$REL_JSON" -w '%{http_code}' -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG")
|
||||
if [ "$CODE" != "200" ]; then echo "::error::релиза с тегом $TAG нет (HTTP $CODE): сначала дождись сборки build.yml на main"; exit 1; fi
|
||||
python3 - "$TAG" "$REL_JSON" <<'PY'
|
||||
import json, os, sys
|
||||
tag, path = sys.argv[1], sys.argv[2]
|
||||
r = json.load(open(path))
|
||||
assets = [a.get('name') for a in (r.get('assets') or [])]
|
||||
if r.get('tag_name') != tag or r.get('name') != tag or 'airdef.hmod' not in assets:
|
||||
print('::error::релиз %s не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем' % tag)
|
||||
sys.exit(1)
|
||||
with open(os.environ['GITHUB_ENV'], 'a') as f:
|
||||
f.write('RELEASE_ID=%s\nRELEASE_SHA=%s\n' % (r['id'], r.get('target_commitish', '')))
|
||||
PY
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: refs/tags/${{ inputs.tag }}
|
||||
- name: Проверка коммита
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -e
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
if [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] && [ "$HEAD_SHA" != "$RELEASE_SHA" ]; then echo "::error::тег $TAG указывает на $HEAD_SHA, а релиз создан из $RELEASE_SHA"; exit 1; fi
|
||||
echo "собираю $TAG = $HEAD_SHA"
|
||||
- name: Системные библиотеки
|
||||
run: |
|
||||
SUDO=$(command -v sudo || true)
|
||||
@@ -45,12 +77,11 @@ jobs:
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -e
|
||||
D=airdef-linux-x86_64
|
||||
mkdir -p "$D" out
|
||||
cp horizon/target/fast/horizon-client horizon/target/fast/horizon-ctl airdef-mod/target/horizon-mod/airdef.hmod dist/play.sh docs/INSTALL.md "$D/"
|
||||
tar czf "out/$D.tar.gz" "$D"
|
||||
ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$ID/assets?name=$D.tar.gz" > /dev/null
|
||||
# RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml)
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$RELEASE_ID/assets?name=$D.tar.gz" > /dev/null
|
||||
|
||||
@@ -27,7 +27,9 @@ python3 tools/airdef.py play # запустить (аргументы игр
|
||||
CI: `.gitea/workflows/build.yml` на каждый пуш собирает только мод (`airdef.hmod`), а пуш в `main` публикует его
|
||||
релизом `build-N`. Игру-форк собирают вручную: `.gitea/workflows/game.yml` (Linux) и `.gitea/workflows/desktop.yml`
|
||||
(macOS и Windows, на раннерах с метками `macos-latest` / `windows-latest`); оба запускаются из Actions с тегом
|
||||
готового релиза и дописывают свои архивы в него.
|
||||
готового релиза `build-N`. Перед сборкой workflow проверяет, что такой релиз создан `build.yml` (имя релиза = тег,
|
||||
есть `airdef.hmod`), иначе падает с ошибкой; затем выкачивает ровно коммит этого тега (а не ветку, с которой запущен),
|
||||
сверяет его с коммитом релиза и дописывает архивы только в этот релиз.
|
||||
|
||||
## Как играть
|
||||
|
||||
|
||||
+66
-8
@@ -8,8 +8,9 @@
|
||||
|
||||
HD — третий пресет графики мода (`/airdef shader off|on|hd`): отложенное освещение в стиле шейдерпаков
|
||||
(BSL/Complementary-класс), но написанное с нуля внутри нашего форка Horizon (Minecraft 26.3 на Rust/wgpu).
|
||||
Код шейдерпаков (BSL, Complementary, Photon, Iris/OptiFine) **не копируем**: их лицензии запрещают перенос, а в
|
||||
Horizon действует правило clean-room. Пишем по знанию техник (статьи, общеизвестные алгоритмы).
|
||||
Код шейдерпаков (BSL, Complementary, Iris/OptiFine и прочих) **не копируем**: их лицензии запрещают перенос, а в
|
||||
Horizon действует правило clean-room. Исключение — Photon (sixthsurge), коммит 26c2906ee02cc484359b835fba6993d443d45966: он под MIT, из него можно
|
||||
переносить идеи и код **с указанием авторства** (комментарий у кода + `gfx/THIRD_PARTY_PHOTON.md`). Остальное пишем по знанию техник (статьи, общеизвестные алгоритмы).
|
||||
|
||||
Пресеты должны оставаться: `off` (ваниль), `on` (пост-эффекты мода), `hd` (этот рендер), позже `rt` (аппаратная
|
||||
трассировка, отдельно).
|
||||
@@ -19,6 +20,7 @@ Horizon действует правило clean-room. Пишем по знани
|
||||
| Что | Где |
|
||||
|---|---|
|
||||
| Весь HD-конвейер: цели, проходы, WGSL-шейдеры (строками), тайлы света, AO, тени, bloom, tonemap, TAA | `horizon/crates/horizon-client/src/gfx/hd.rs` |
|
||||
| Атмосфера: сборка текстуры неба (WGSL), чтение неба/дымки/тона света в шейдерах, цвет солнца (`sun_transmittance`) | `horizon/crates/horizon-client/src/gfx/hd_sky.rs` (+ `THIRD_PARTY_PHOTON.md`) |
|
||||
| Встраивание в кадр: G-буфер-проход, каскады теней, вызовы `hd.light` / `hd.finish`, джиттер, тайминги | `horizon/crates/horizon-client/src/gfx.rs` (`Renderer::render`, ищи `hd_on`, `AirDef fork:`) |
|
||||
| G-буфер террейна: `fs_gbuffer`, `fs_shadow` | `horizon/crates/horizon-client/src/terrain.wgsl` |
|
||||
| G-буфер сущностей и моделей мода: `fs_entity_gbuffer`, `fs_entity_shadow` | `horizon/crates/horizon-client/src/entity.wgsl` |
|
||||
@@ -43,6 +45,7 @@ cd .. && rm -f horizon-patches/*.patch && git -C horizon format-patch -q $(cat H
|
||||
(`Depth32Float`, 1 = близко, 0 = далеко). Позиции везде относительно камеры (camera-relative), мир — в `f64`
|
||||
на CPU.
|
||||
|
||||
0. **Небо** (`hd.sky`, проход `hd sky`, перед освещением): текстура 128×72 (`Rgba16Float`): 128 азимутов (от солнца, 0..π) × 64 высот (v = √(h/(π/2))), плюс строка 64 с «окружающим светом» (текселы 0 — зенит, 1 — среднее у горизонта). Каждый тексел — марш по лучу (32 шага) однократного рассеяния Рэлея/Ми + озон, плоский член многократного рассеяния (`MULTI_SCATTER`), солнце и луна (луна — напротив) как источники; пропускание до источника — аналитическое (Chapman, как у Photon). Зависит только от высоты солнца → перерисовывается, лишь когда она сдвинулась больше чем на 0.001. Читают её `fs_light` и композит воды (привязки 15 и 10).
|
||||
1. **Каскады теней** (2 шт., `CASCADES = [40, 176]` блоков полуразмера, `SHADOW_SIZE = 2048`, массив глубины
|
||||
из 2 слоёв). Источник — солнце днём, луна ночью (`light_of`). Матрицы `cascades()` привязаны к текселям
|
||||
(тени не «ползут»). Во время прохода буфер глобалов мира подменяется на глобалы каскада (copy-трюк) и потом
|
||||
@@ -50,13 +53,29 @@ cd .. && rm -f horizon-patches/*.patch && git -C horizon format-patch -q $(cat H
|
||||
(`shadow_casters`: без оверлеев 4/9/10/40 — 40 это светящиеся модели мода).
|
||||
2. **G-буфер** (свой depth): albedo `Rgba8Unorm` (a = эмиссия), нормаль `Rgba16Float` (мировая, из `dpdx/dpdy`),
|
||||
уровни света `Rgba8Unorm` (x = небо, y = блоки — сырые координаты лайтмапа /240). Террейн + дальние LOD-тайлы +
|
||||
сущности. Вода/полупрозрачное в G-буфер **не** попадает (идёт ванильным путём) — см. план.
|
||||
3. **GTAO** (полразрешения, `Rg16Float`: окклюзия, дистанция) + depth-aware блюр 5×5. 3 среза × 5 шагов, радиус
|
||||
сущности. Полупрозрачное в G-буфер **не** попадает (идёт ванильным путём); вода — отдельный путь, см. п. 3 и «Вода».
|
||||
3. **Вода** (`water_pass`, только если на экране есть полупрозрачные секции, нет `frame.underwater` и не `HORIZON_HD_WATER=0`):
|
||||
те же секции ещё раз с `fs_water` (terrain.wgsl), который отбрасывает всё, кроме слоёв спрайтов `water_still`/`water_flow`
|
||||
(номера слоёв передаются в `Renderer::new` из `sprite_layers` и ставятся override-константами `WATER_STILL/WATER_FLOW`; стекло,
|
||||
лёд и прочее остаются ванильными). Цели: цвет (спрайт × биомный тинт, a = свет блоков, `Rgba16Float`), нормаль грани (a = свет неба,
|
||||
`Rgba8Unorm`) и собственная глубина воды (`Depth32Float`, 0 = воды нет; тест глубины только среди воды, перекрытие
|
||||
землёй решают потребители). Пайплайны `gbuffer_water` (секции) и `flat.water` (flat-путь).
|
||||
3a. **GTAO** (полразрешения, `Rg16Float`: окклюзия, дистанция) + depth-aware блюр 5×5. 3 среза × 5 шагов, радиус
|
||||
1.8 блока, шум IGN по кадру. Сила — `HdFrame.ambient.w` (`HORIZON_HD_AO=<число>`, 0 = выкл).
|
||||
4. **Освещение** `fs_light` → `lit` (HDR): `max(небо·ambient.x, блоки·ambient.y)·AO + солнце·N·L·тень + точечные
|
||||
4. **Освещение** `fs_light` → `lit` (HDR): пиксель неба (глубина мира 0, Оверворлд, без env-тумана) — это небо атмосферы + то, что мир нарисовал поверх ванильного неба (`max(scene − vanilla_sky(view), 0)`: спрайты луны, звёзды; квадрат ванильного солнца вырезается вокруг направления солнца и заменяется круглым диском с гало); облака (глубина > 0, нет в G-буфере, y > 8, дальше 48 блоков) на закате/рассвете окрашиваются цветом солнца. Свет неба: ванильный `lightmap` × подкраска цветом неба (зенит сверху, горизонт сбоку; яркость 1) × форма по нормали (вверх 1.1, вниз 0.66), только там, где есть небесный свет. Тени: ближний каскад — PCSS-lite (поиск блокировщиков 8 выборок, ширина полутени от расстояния до блокировщика, 12 выборок спирали Фогеля, повёрнутых IGN; сглаживает TAA), дальний — 3×3. Туман: цвет — небо над горизонтом в направлении пикселя (`haze_colour`) + воздушная перспектива `1 − exp(−d/700)` × 0.5. Далее прежнее: пиксель — вода, если `water_depth >= g` и перед водой в мировой глубине ничего нет
|
||||
(`d - wd <= 0.2%`); тогда освещается **дно** по данным G-буфера (одна выборка тени, без AO), а в альфу `lit` пишется 0.5 (признак воды). `max(небо·ambient.x, блоки·ambient.y)·AO + солнце·N·L·тень + точечные
|
||||
источники` × albedo + эмиссия; затем ванильный туман заново, объёмные лучи (14 шагов, Henyey-Greenstein g=0.6),
|
||||
ореолы вокруг источников. Точечные источники — по тайлам 16×16 (`tile_lists`, буфер `tiles`: на тайл 4 слова —
|
||||
начало/длина списка для поверхностей и для ореолов, потом индексы; переполнение → «все источники»).
|
||||
4a. **Композит воды** `fs_water` (`hd.water_composite`, `lit` → `lit_water`, дальше везде `hd.lit_view()`; на пикселях без воды копия).
|
||||
Волны — сумма 10 синусоид в мировых координатах (векторы — целые кратные 2π/256, скорости — целые циклы за 600 с: период 256 блоков
|
||||
и 10 минут, не «плывут» с камерой; мелкие волны гасятся по размеру пикселя против мерцания); нормаль = нормаль грани + градиент
|
||||
(только на горизонтальных гранях). Преломление: смещение луча относительно плоской воды, выборка `lit` по uv со сдвигом (отбраковка
|
||||
по глубине: не берутся банк/сущности перед водой); толщина = расстояние от поверхности до дна; поглощение `exp(-(1-цвет)·0.8·(t+0.3))`
|
||||
+ рассеяние цветом воды при свете «объёма» (лайтмап + солнце с тенью). Отражение: SSR по глубине G-буфера (24 шага, растущие,
|
||||
джиттер IGN по кадру, 5 шагов бинарного уточнения, толщина `0.4+0.08·s`), при промахе — небо/туман (`sky_color`, `fog_color`, тусклее
|
||||
под навесом). Волны: к 10 крупным добавлены 16 мелких (длины 0.35–3 блока, уклон до 0.05, гасятся по размеру пикселя) — густая мелкая рябь для искорок блика; блик GGX с a=0.07. Цвет воды светлее: биом × 0.85, поглощение 0.8, рассеяние 0.85 (ночью до 0.4). Небо отражения и туман — из атмосферы. Френель Шлика F0=0.02 (SSR пропускается при F<0.04: взгляд вниз), блик солнца GGX (a=0.05) × тень, туман поверх.
|
||||
Подводный вид и `HORIZON_HD_WATER=0` — композит и проход воды не работают, `lit` как раньше.
|
||||
5. **Пост-проход мода** `lit` → `fx` (`effects.wgsl`: взрывы, дым, вспышки, свечение; при HD его собственная
|
||||
«подсветка огнём» выключена — свет даёт HD).
|
||||
6. **TAA** — после пост-прохода мода (`fx`, либо `lit` без него), до bloom: история пинг-понгом, в альфе истории —
|
||||
@@ -88,10 +107,14 @@ Bind groups кэшируются и пересоздаются только пр
|
||||
| 10, 11 | лайтмап, линейный сэмплер |
|
||||
| 12 | тайлы источников (storage) |
|
||||
| 13 | AO |
|
||||
| 14 | глубина воды (`water_depth`) |
|
||||
| 15 | текстура неба (`sky_lut`) |
|
||||
|
||||
`HdFrame` (Rust и WGSL держать в синхроне!): `inverse_view_proj`, `resolution` (w, h, 1/w, 1/h), `params` (число
|
||||
источников, экспозиция, bloom, счётчик кадров), `cascades[2]`, `sun` (направление, сила), `sun_color` (rgb, ночь),
|
||||
`fog_color`, `fog`, `ambient` (тень неба, блоки, плотность воздуха, AO), `shadow` (вкл, R0, R1, тайлов в строке).
|
||||
`fog_color`, `fog`, `ambient` (тень неба, блоки, плотность воздуха, AO), `shadow` (вкл, R0, R1, тайлов в строке), `prev_view_proj`, `taa`, `taa_params`, `view_proj` (с джиттером, для лучей воды), `water` (x: эффект идёт, y: секунды волн mod 600, zw: камера x, z mod 256), `sky_color` (w: есть небо), `sky_sun` (xyz: на само солнце, w: 1 — небо рисует атмосфера), `sky_extra` (x: конец ванильного тумана неба, z: пасмурность от дождя 0..0.85), `sunrise` (ванильная полоса восхода/заката — вычитается из мира).
|
||||
|
||||
Композит воды (`WATER`, group 0): 0 `HdFrame`, 1 `lit`, 2 глубина G-буфера, 3 цвет воды, 4 нормаль воды, 5 глубина воды, 6–7 тени, 8 лайтмап, 9 сэмплер, 10 текстура неба. Проход неба (`SKY_PASS`, group 0): только `HdFrame`.
|
||||
|
||||
## Как проверять
|
||||
|
||||
@@ -146,6 +169,9 @@ TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс
|
||||
| TAA: джиттер Halton(2,3) ×16, перепроекция по глубине и сдвигу камеры, variance clip в YCoCg, история Catmull-Rom 0.9, лёгкий шарп в tonemap (`HORIZON_HD_TAA=0` — выкл) | готово | патч 16 |
|
||||
| TAAU: мир в `HORIZON_HD_SCALE` (0.25–1, по умолчанию 1 = как раньше), восстановление в TAA-проходе `fs_taau`; без LOD-bias текстур (не доступен) | готово | патч 18 |
|
||||
| Исправлено мерцание теней у игрока: ближний каскад терял секции в кадры, когда дальний держится; держимая матрица дальнего каскада сдвигается на шаг камеры; у растений (наклонные квады) нормаль «вверх», а не к камере; TAA держит 94% истории в покое | готово | патч 18 |
|
||||
| Вода: проход `fs_water`, преломление, SSR, Френель, блик (`HORIZON_HD_WATER=0` — выкл) | готово | патч 19 (+20: без ванильной текстуры на поверхности, цвет темнее и глубже) |
|
||||
| Вода: светлее, густая мелкая рябь (26 волн), блик a=0.07 (мерцание на реке ≈1.4% пикселей >20/255) | готово | патч 21 |
|
||||
| Небо и атмосфера (Рэлей/Ми/озон, текстура неба, воздушная перспектива, цвет солнца из пропускания, голубоватый ambient по нормали, PCSS-lite ближнего каскада, диск солнца, цвет облаков на закате; порт констант и Chapman из Photon, MIT) | готово | патч 22 |
|
||||
| API моделей по хэндлам (#30, уровень API 11: `model-id`, `model-generation`, `set-models-by-id`) | готово | патч 17 |
|
||||
|
||||
## План (по порядку) и ТЗ
|
||||
@@ -158,10 +184,10 @@ TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс
|
||||
масштаба (`HORIZON_HD_SCALE` или поле в `HdSettings`). Цель: при 0.67 картинка близка к нативной, FPS заметно
|
||||
выше. Альтернатива на Mac — MetalFX через wgpu-hal (нативный код, только Metal) — отдельной опцией потом. DLSS
|
||||
нереален для нашей цели (только NVIDIA через Streamline, нет в wgpu).
|
||||
2. **Вода**: отдельный G-буфер-путь для воды (маска + нормаль с анимированными волнами), SSR (трассировка по
|
||||
2. **Вода** — СДЕЛАНО (патч 19, см. «Вода» ниже). Исходное ТЗ: отдельный G-буфер-путь для воды (маска + нормаль с анимированными волнами), SSR (трассировка по
|
||||
глубине, откат на небо/туман), преломление (смещение по нормали), затухание по глубине воды (цвет биома),
|
||||
блики солнца (GGX-подобный), френель. Подводный вид не ломать (`frame.underwater`).
|
||||
3. **Небо и атмосфера**: рассеяние Рэлея/Ми (предвычисленная LUT пропускания и небесного цвета в маленькой
|
||||
3. **Небо и атмосфера** — СДЕЛАНО (патч 22, см. «Небо и свет» ниже). Исходное ТЗ: рассеяние Рэлея/Ми (предвычисленная LUT пропускания и небесного цвета в маленькой
|
||||
текстуре), солнце/луна дисками, звёзды из ванили; туман и лучи берут цвет из той же модели. Учесть дождь
|
||||
(`weather`), закат, Незер/Энд (там — ванильное небо).
|
||||
4. **Объёмные облака** (рэймарч слоя с шумом) + их тени на земле (в карту/маску солнца).
|
||||
@@ -174,8 +200,32 @@ TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс
|
||||
10. **RT-пресет**: `wgpu` `EXPERIMENTAL_RAY_QUERY` (Vulkan/DX12/Metal M3+), BLAS по секциям, ray-query тени и
|
||||
отражения, затем GI. Отдельный пресет, HD остаётся.
|
||||
|
||||
## Вода (патч 19): что знать преемнику
|
||||
|
||||
- Тайминги проходов: `hd water` (геометрия воды; на Metal его метка часто 0 — работа учитывается в соседних проходах) и `hd water composite`.
|
||||
Замеры 2560×1440, Hills, река на ~40% экрана, M5 (шумно: у меня GPU делят браузеры): композит 0.8–2 мс, `hd lighting` на 2–3 мс
|
||||
выше, чем без воды (теперь освещается дно под водой — раньше эти пиксели были ванильными); всего вода ≈ +3 мс при большой воде
|
||||
на экране, около 0.5–1 мс при малой. Цель «≤ 2 мс» при полной реке не достигнута. Идеи: отражения в полразрешения (с учётом маски воды
|
||||
на границе), SSR только при F>0.04 уже есть, ужать освещение дна (без точечных источников на глубине > N блоков).
|
||||
- Вода находится по слоям спрайтов. LOD-тайлы воды (`lod/mesh.rs`) идут тем же путём, если их слой — water_still; вода из resource pack с
|
||||
другим именем спрайта не распознается (останется ванильной). `water_overlay` (боковые грани у стекла) не трогаем.
|
||||
- Стекло/частицы/лёд перед водой: проверка `d - wd` возвращает ванильный вид пикселя (вода под стеклом без эффекта).
|
||||
- Известные ограничения: отражается только то, что есть на экране (и только непрозрачное: листва — да, вода/стекло — нет); дождь на воде,
|
||||
пена у берега, подводная каустика не сделаны; ночью вода слегка ярче неба (нужно привязать к модели неба — п. 4 плана «Небо»); отражение
|
||||
солнечного диска — только блик GGX (без диска в SSR). Проверка мерцания на реке: разница двух скриншотов ≈1.6% пикселей >20/255 (норма ~2%).
|
||||
|
||||
## Небо и свет (патч 22): что знать преемнику
|
||||
|
||||
- Тайминги (M5, 2560×1440, Hills, река в кадре): `hd sky` ≈ 2.4–4.7 мс по таймстемпам, но кадр не вырос (GPU avg 18.4 мс против 18.1) — на Metal метка крошечного прохода ловит ожидание соседних; с кэшем по высоте солнца в покое проход не выполняется. Освещение 4.0–5.1 мс (PCSS-lite и небо почти не добавили), композит воды 0.6–2.4, TAA 2.6–2.9.
|
||||
- Яркость: небо — физическая радиация → сжатие по максимальному каналу (`SKY_EXPOSURE` 0.85) → гамма 1/1.9 → насыщенность 1.2 (+0.45 у низкого солнца). Весь конвейер в гамма-пространстве, поэтому цвета неба и подкраски света — «показанные» значения; подкраска света неба имеет яркость 1 и не меняет экспозицию (яркость задаёт ванильный лайтмап × `ambient.x` = 0.62 днём, 0.95 ночью).
|
||||
- Цвет солнца (`light_of`): пропускание атмосферы вдоль солнца (`hd_sky::sun_transmittance`), нормировано на 1, степень 0.6, × (1, 0.97, 0.88). Луна — холодная, как раньше.
|
||||
- Дождь: `sky_extra.z` смешивает небо/дымку с ванильным градиентом (серым от ванили); Нижний мир/Энд/подводное — `sky_sun.w = 0`, всё как раньше (ванильное небо и туман).
|
||||
- Облака — по-прежнему ванильные блоки; объёмные облака — п. 4 плана. Окраска облаков цветом закатного солнца — эвристика по маске (глубина есть, G-буфера нет, высота и дальность), может задеть далёкие частицы.
|
||||
- Идеи дальше: многократное рассеяние по Hillaire вместо плоского члена, тени облаков, диск луны с фазой, звёзды/галактика по Photon (`sky.glsl`), розовый оттенок заката (`get_sun_tint` у Photon).
|
||||
|
||||
## Правила
|
||||
|
||||
- Код Photon (MIT) переносить можно только с атрибуцией (см. «Что это»); код других шейдерпаков — нельзя.
|
||||
- Ничего не ломать в `off`/`on`; HD выключен — код HD не работает вовсе (`hd = None`).
|
||||
- Без потери качества ради скорости без договорённости (шаги, разрешение, число эффектов).
|
||||
- Каждый новый WGSL — в naga-тест. Каждый новый проход — в тайминги (`PassTiming`).
|
||||
@@ -183,3 +233,11 @@ TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс
|
||||
- Коммиты: в форке, затем патчи; подпись `Co-Authored-By: Claude <модель> <noreply@anthropic.com>`.
|
||||
- Пуш в Gitea с машины без DNS: `ssh://git@100.92.6.40:2222/chexit/airdef.git` (ключ `~/Documents/claude-keys/deploy`).
|
||||
- Обнови этот документ (статус + что изменилось в конвейере) в том же коммите, что и код.
|
||||
|
||||
## Тюнинг после проверки на полигоне (патч 22)
|
||||
|
||||
Голубая заливка неба по умолчанию делала белый бетон «снегом»: оттенок неба в заливке ослаблен до 0.4
|
||||
(`sky_light_tint`, в `fs_light`), заливка днём `ambient.x` = 0.5 (+0.45 ночью), солнце 0.7 и теплее
|
||||
(`light_of`: множитель (1, 0.93, 0.78)). Проверять баланс на `Polygon` в полдень (`time set 6000`) — белое должно
|
||||
оставаться белым с тёплым оттенком.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:37:45 +0300
|
||||
Subject: [PATCH 01/18] Mod API (AirDef fork): world models, client particles,
|
||||
Subject: [PATCH 01/22] Mod API (AirDef fork): world models, client particles,
|
||||
sounds, ground heights
|
||||
|
||||
render.set-models draws block models with an interpolated pose per id;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:39:45 +0300
|
||||
Subject: [PATCH 02/18] cargo-horizon-mod: report particles and sounds
|
||||
Subject: [PATCH 02/22] cargo-horizon-mod: report particles and sounds
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 13:33:10 +0300
|
||||
Subject: [PATCH 03/18] Mod API (AirDef fork): full-screen post pass for mods
|
||||
Subject: [PATCH 03/22] Mod API (AirDef fork): full-screen post pass for mods
|
||||
|
||||
render.set-post compiles a mod's WGSL after a prelude (camera matrices,
|
||||
time, 256 vec4 of params, the world's colour and depth) and runs it after
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:09:30 +0300
|
||||
Subject: [PATCH 04/18] Mod models: draw far models as scaled impostors past
|
||||
Subject: [PATCH 04/22] Mod models: draw far models as scaled impostors past
|
||||
the far plane, cull at 4096
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:31:00 +0300
|
||||
Subject: [PATCH 05/18] Mod cameras: render.set-views draws the world from
|
||||
Subject: [PATCH 05/22] Mod cameras: render.set-views draws the world from
|
||||
other points into pictures that HUD view shapes show (API level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:08:21 +0300
|
||||
Subject: [PATCH 06/18] Mods: more fuel per tick and per event (AirDef plans
|
||||
Subject: [PATCH 06/22] Mods: more fuel per tick and per event (AirDef plans
|
||||
raids)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:25:29 +0300
|
||||
Subject: [PATCH 07/18] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
Subject: [PATCH 07/22] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:15:11 +0300
|
||||
Subject: [PATCH 08/18] Mod cameras: distant terrain tiles, fog measured from
|
||||
Subject: [PATCH 08/22] Mod cameras: distant terrain tiles, fog measured from
|
||||
the camera, no impostors while one is shown
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:25:45 +0300
|
||||
Subject: [PATCH 09/18] Mods: game.world-id, so mods keep state per world (API
|
||||
Subject: [PATCH 09/22] Mods: game.world-id, so mods keep state per world (API
|
||||
level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 12:01:08 +0300
|
||||
Subject: [PATCH 10/18] cargo-horizon-mod: trial runs behind feature full, so a
|
||||
Subject: [PATCH 10/22] cargo-horizon-mod: trial runs behind feature full, so a
|
||||
packing-only tool skips wasmtime
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 0a56f5616df4699588cbcc529d4720661baa810f Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 13:18:43 +0300
|
||||
Subject: [PATCH 11/18] HD lighting: G-buffer pass, per-pixel mod point lights
|
||||
Subject: [PATCH 11/22] HD lighting: G-buffer pass, per-pixel mod point lights
|
||||
in HDR, bloom and tone curve; render.set-lights and render.set-lighting (API
|
||||
level 10)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 9ebf0031fe9ba6b7f02afb26e4fd3a1ba4ddb0d0 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 13:59:43 +0300
|
||||
Subject: [PATCH 12/18] HD lighting stage 2: sun and moon shadow cascades, the
|
||||
Subject: [PATCH 12/22] HD lighting stage 2: sun and moon shadow cascades, the
|
||||
world relit (sun by face angle and shadow, sky and block light through the
|
||||
lightmap), light shafts and halos, fog over it again, a light colour grade
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 7a5a38f68923aec62f10bfa0df50b436474ed612 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 16:19:53 +0300
|
||||
Subject: [PATCH 13/18] HD: bind groups cached until a resize or a new source,
|
||||
Subject: [PATCH 13/22] HD: bind groups cached until a resize or a new source,
|
||||
shadow passes draw only the entities that cast shadows
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 8020811e57d4b5a9ea1cd72d30ce859dc5095d35 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 16:25:44 +0300
|
||||
Subject: [PATCH 14/18] HD: lights culled per 16x16 tile (surface and halo
|
||||
Subject: [PATCH 14/22] HD: lights culled per 16x16 tile (surface and halo
|
||||
lists built on the CPU), a naga test for the lighting shaders
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 72d3fddfea329e578162656151350f0aa2efd323 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 16:43:36 +0300
|
||||
Subject: [PATCH 15/18] HD: GPU time per pass in the F3 timing list,
|
||||
Subject: [PATCH 15/22] HD: GPU time per pass in the F3 timing list,
|
||||
half-resolution GTAO with a depth-aware blur
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 1fd83a821bf270879d8ece5aa583f200ed920d89 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:02:02 +0300
|
||||
Subject: [PATCH 16/18] HD: shadows draw only sun-facing quads and skip buried
|
||||
Subject: [PATCH 16/22] HD: shadows draw only sun-facing quads and skip buried
|
||||
sections, the far cascade every second frame; TAA (Halton jitter,
|
||||
reprojection by depth, YCoCg variance clipping) before bloom
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 1502d359d599228fe0f44ce6b99a3fded2ac5ea0 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:02:02 +0300
|
||||
Subject: [PATCH 17/18] Mods: model handles (render.model-id, model-generation,
|
||||
Subject: [PATCH 17/22] Mods: model handles (render.model-id, model-generation,
|
||||
set-models-by-id; API level 11) so mods send no model names each tick;
|
||||
clear-post and set-lighting documented
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 0f9d99b8ad3e4b72e621d84c2396cf1d3d2b7a33 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:19:57 +0300
|
||||
Subject: [PATCH 18/18] HD: temporal upscaling (HORIZON_HD_SCALE: world,
|
||||
Subject: [PATCH 18/22] HD: temporal upscaling (HORIZON_HD_SCALE: world,
|
||||
G-buffer and lighting at a lower size, rebuilt to the window in the TAA
|
||||
pass); fix flickering shadows near the player (the near cascade lost its
|
||||
sections on frames the far one was held, the held far matrix ignored the
|
||||
|
||||
@@ -0,0 +1,982 @@
|
||||
From 38250c4392e27d5700842ca6ee47291dd1831b72 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:47:52 +0300
|
||||
Subject: [PATCH 19/22] HD water: its own pass (colour, normal, depth), the
|
||||
floor under it lit, a composite with world-space waves, refraction with depth
|
||||
absorption in the biome colour, screen-space reflections, Fresnel and a
|
||||
shadowed sun glint (HORIZON_HD_WATER=0 to switch off)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
crates/horizon-client/src/game.rs | 9 +
|
||||
crates/horizon-client/src/gfx.rs | 109 +++++-
|
||||
crates/horizon-client/src/gfx/flat.rs | 4 +
|
||||
crates/horizon-client/src/gfx/hd.rs | 497 ++++++++++++++++++++++++-
|
||||
crates/horizon-client/src/terrain.wgsl | 19 +
|
||||
5 files changed, 627 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs
|
||||
index 3a11ee283..5a173977f 100644
|
||||
--- a/crates/horizon-client/src/game.rs
|
||||
+++ b/crates/horizon-client/src/game.rs
|
||||
@@ -14658,6 +14658,14 @@ fn build_renderer(
|
||||
prepared,
|
||||
previous,
|
||||
api,
|
||||
+ // AirDef fork, HD water: the layers of the water sprites.
|
||||
+ ["water_still", "water_flow"].map(|name| {
|
||||
+ assets
|
||||
+ .sprite_layers
|
||||
+ .get(&format!("minecraft:block/{name}"))
|
||||
+ .copied()
|
||||
+ .unwrap_or(0xffff)
|
||||
+ }),
|
||||
))?;
|
||||
renderer.hud.sprite_animations = atlas
|
||||
.sprite_animations
|
||||
@@ -14717,6 +14725,7 @@ fn loading_renderer(
|
||||
None,
|
||||
previous,
|
||||
api,
|
||||
+ [0xffff; 2],
|
||||
))?;
|
||||
if let Some(atlas) = atlas {
|
||||
renderer.hud.sprite_animations = atlas
|
||||
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
|
||||
index b85c6e269..f9dab8abf 100644
|
||||
--- a/crates/horizon-client/src/gfx.rs
|
||||
+++ b/crates/horizon-client/src/gfx.rs
|
||||
@@ -564,6 +564,8 @@ pub struct Renderer {
|
||||
mod_views: Option<mod_views::Views>,
|
||||
/// AirDef fork: the HD lighting's G-buffer pipelines (terrain, entities) and its targets.
|
||||
gbuffer_terrain: wgpu::RenderPipeline,
|
||||
+ /// The water quads alone into the HD water targets (section draws; `flat.water` for flat ones).
|
||||
+ gbuffer_water: wgpu::RenderPipeline,
|
||||
gbuffer_entity: wgpu::RenderPipeline,
|
||||
/// The HD sun shadows: depth only, from the light, and the sections in its farther cascade.
|
||||
shadow_terrain: wgpu::RenderPipeline,
|
||||
@@ -1083,6 +1085,7 @@ impl Renderer {
|
||||
prepared: Option<PreparedTextures>,
|
||||
previous: Option<&Self>,
|
||||
api: GraphicsApi,
|
||||
+ water_layers: [u32; 2],
|
||||
) -> Result<Self> {
|
||||
let split = sprite_split();
|
||||
if layers > split * SPRITE_ARRAYS {
|
||||
@@ -1205,6 +1208,13 @@ impl Renderer {
|
||||
("SPRITE_ARRAYS", f64::from(sprite_arrays)),
|
||||
("SPRITE_SPLIT", f64::from(split)),
|
||||
];
|
||||
+ // AirDef fork, HD water: the sprite layers terrain.wgsl's fs_water takes for water.
|
||||
+ let water_constants = [
|
||||
+ ("SPRITE_ARRAYS", f64::from(sprite_arrays)),
|
||||
+ ("SPRITE_SPLIT", f64::from(split)),
|
||||
+ ("WATER_STILL", f64::from(water_layers[0])),
|
||||
+ ("WATER_FLOW", f64::from(water_layers[1])),
|
||||
+ ];
|
||||
let bindings = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Horizon terrain bindings"),
|
||||
entries: &[
|
||||
@@ -1420,6 +1430,55 @@ impl Renderer {
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
+ // AirDef fork, HD water: only the water quads (`fs_water`), into the colour and normal
|
||||
+ // targets and the water's own depth; `vertex` is the section or the flat vertex stage.
|
||||
+ let water_targets = [
|
||||
+ Some(wgpu::ColorTargetState {
|
||||
+ format: hd::WATER_TINT,
|
||||
+ blend: None,
|
||||
+ write_mask: wgpu::ColorWrites::ALL,
|
||||
+ }),
|
||||
+ Some(wgpu::ColorTargetState {
|
||||
+ format: hd::WATER_NORMAL,
|
||||
+ blend: None,
|
||||
+ write_mask: wgpu::ColorWrites::ALL,
|
||||
+ }),
|
||||
+ ];
|
||||
+ let water_pipeline =
|
||||
+ |layout: &wgpu::PipelineLayout,
|
||||
+ label: &str,
|
||||
+ vertex: &str,
|
||||
+ buffers: &[Option<wgpu::VertexBufferLayout<'_>>]| {
|
||||
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
+ label: Some(label),
|
||||
+ layout: Some(layout),
|
||||
+ vertex: wgpu::VertexState {
|
||||
+ module: &shader,
|
||||
+ entry_point: Some(vertex),
|
||||
+ compilation_options: Default::default(),
|
||||
+ buffers,
|
||||
+ },
|
||||
+ primitive: wgpu::PrimitiveState {
|
||||
+ cull_mode: Some(wgpu::Face::Back),
|
||||
+ ..Default::default()
|
||||
+ },
|
||||
+ depth_stencil: gbuffer_depth.clone(),
|
||||
+ multisample: Default::default(),
|
||||
+ fragment: Some(wgpu::FragmentState {
|
||||
+ module: &shader,
|
||||
+ entry_point: Some("fs_water"),
|
||||
+ compilation_options: wgpu::PipelineCompilationOptions {
|
||||
+ constants: &water_constants,
|
||||
+ ..Default::default()
|
||||
+ },
|
||||
+ targets: &water_targets,
|
||||
+ }),
|
||||
+ multiview_mask: None,
|
||||
+ cache: None,
|
||||
+ })
|
||||
+ };
|
||||
+ let gbuffer_water =
|
||||
+ water_pipeline(&layout, "HD water terrain", "vs_terrain", &terrain_buffers);
|
||||
let shadow_terrain = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("HD shadows terrain"),
|
||||
layout: Some(&layout),
|
||||
@@ -2597,6 +2656,7 @@ impl Renderer {
|
||||
&device,
|
||||
&bindings,
|
||||
arena.page_vertices(),
|
||||
+ |layout, label| water_pipeline(layout, label, "vs_terrain_flat", &buffers),
|
||||
|layout, label, fragment, blend| {
|
||||
pipeline_in(
|
||||
layout,
|
||||
@@ -2816,6 +2876,7 @@ impl Renderer {
|
||||
mod_post_hdr: false,
|
||||
mod_views: None,
|
||||
gbuffer_terrain,
|
||||
+ gbuffer_water,
|
||||
gbuffer_entity,
|
||||
shadow_terrain,
|
||||
shadow_entity,
|
||||
@@ -4204,6 +4265,16 @@ impl Renderer {
|
||||
fog_color,
|
||||
fog,
|
||||
sky: frame.skybox && !frame.end_sky && !frame.nether_lighting && !frame.underwater,
|
||||
+ sky_color: [
|
||||
+ frame.sky.sky_color[0],
|
||||
+ frame.sky.sky_color[1],
|
||||
+ frame.sky.sky_color[2],
|
||||
+ ],
|
||||
+ camera_mod: [
|
||||
+ camera.x.rem_euclid(256.0) as f32,
|
||||
+ camera.z.rem_euclid(256.0) as f32,
|
||||
+ ],
|
||||
+ water: !frame.underwater,
|
||||
};
|
||||
let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene);
|
||||
let mut hd_cascades = hd::cascades(camera, shadow_dir);
|
||||
@@ -5058,6 +5129,11 @@ impl Renderer {
|
||||
.hd
|
||||
.get_or_insert_with(|| hd::Hd::new(device, size, format, globals_size));
|
||||
hd.resize(device, size, [self.config.width, self.config.height]);
|
||||
+ // Translucent terrain on screen: the water pass has something to draw.
|
||||
+ hd.water_geometry = match self.flat.as_ref().filter(|_| flat_frame) {
|
||||
+ Some(flat) => !flat.translucent_calls().is_empty(),
|
||||
+ None => translucent_draws > 0,
|
||||
+ };
|
||||
if let Some(settings) = frame.hd {
|
||||
hd.write(&self.queue, &hd_scene, frame.hd_lights, settings);
|
||||
}
|
||||
@@ -5186,6 +5262,32 @@ impl Renderer {
|
||||
drop(pass);
|
||||
// Contact shadows from the G-buffer's depth and normals, for the lighting.
|
||||
hd.ao(&mut encoder);
|
||||
+ // The water quads alone, with their own depth: the lighting finds the water by it and
|
||||
+ // the composite after the lighting lays the surface over the lit floor.
|
||||
+ if hd.water_active() {
|
||||
+ let mut pass = hd.water_pass(&mut encoder);
|
||||
+ pass.set_bind_group(0, &self.group, &[]);
|
||||
+ match self.flat.as_ref().filter(|_| flat_frame) {
|
||||
+ Some(flat) => {
|
||||
+ pass.set_pipeline(&flat.water);
|
||||
+ flat.draw(&mut pass, flat.translucent_calls(), |p| {
|
||||
+ self.arena.buffer(p)
|
||||
+ });
|
||||
+ }
|
||||
+ None => {
|
||||
+ pass.set_pipeline(&self.gbuffer_water);
|
||||
+ pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
+ pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
+ self.draw_sections(
|
||||
+ &mut pass,
|
||||
+ &self.indirect,
|
||||
+ 0,
|
||||
+ solid_draws,
|
||||
+ translucent_draws,
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
} else if !hd_on {
|
||||
self.hd = None;
|
||||
}
|
||||
@@ -5721,6 +5823,11 @@ impl Renderer {
|
||||
(color, depth),
|
||||
(&self.lightmap_view, &self.lightmap_sampler),
|
||||
);
|
||||
+ hd.water_composite(
|
||||
+ &self.device,
|
||||
+ &mut encoder,
|
||||
+ (&self.lightmap_view, &self.lightmap_sampler),
|
||||
+ );
|
||||
match &mut self.mod_post {
|
||||
Some(post) if mod_post => {
|
||||
let size = self.world_post.size().unwrap_or([1, 1]);
|
||||
@@ -5731,7 +5838,7 @@ impl Renderer {
|
||||
&self.mod_params.0,
|
||||
self.mod_params.1,
|
||||
);
|
||||
- post.run(&self.device, &mut encoder, (&hd.lit, depth), &hd.fx);
|
||||
+ post.run(&self.device, &mut encoder, (hd.lit_view(), depth), &hd.fx);
|
||||
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Fx, depth);
|
||||
hd.finish(&self.device, &mut encoder, source, &view_target);
|
||||
}
|
||||
diff --git a/crates/horizon-client/src/gfx/flat.rs b/crates/horizon-client/src/gfx/flat.rs
|
||||
index 5d86888da..7e2e20cad 100644
|
||||
--- a/crates/horizon-client/src/gfx/flat.rs
|
||||
+++ b/crates/horizon-client/src/gfx/flat.rs
|
||||
@@ -64,6 +64,8 @@ pub struct Call {
|
||||
pub struct Flat {
|
||||
pub solid: wgpu::RenderPipeline,
|
||||
pub translucent: wgpu::RenderPipeline,
|
||||
+ /// The water quads alone, into HD's water targets.
|
||||
+ pub water: wgpu::RenderPipeline,
|
||||
flatten: wgpu::ComputePipeline,
|
||||
compute_layout: wgpu::BindGroupLayout,
|
||||
flatten_seen: wgpu::ComputePipeline,
|
||||
@@ -254,6 +256,7 @@ impl Flat {
|
||||
device: &wgpu::Device,
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
page_vertices: u64,
|
||||
+ water: impl Fn(&wgpu::PipelineLayout, &str) -> wgpu::RenderPipeline,
|
||||
pipeline: impl Fn(
|
||||
&wgpu::PipelineLayout,
|
||||
&str,
|
||||
@@ -320,6 +323,7 @@ impl Flat {
|
||||
"fs_translucent",
|
||||
Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
),
|
||||
+ water: water(&layout, "HD water terrain (flat)"),
|
||||
flatten,
|
||||
compute_layout,
|
||||
flatten_seen,
|
||||
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
||||
index c54b7b961..f5caa2d0b 100644
|
||||
--- a/crates/horizon-client/src/gfx/hd.rs
|
||||
+++ b/crates/horizon-client/src/gfx/hd.rs
|
||||
@@ -16,6 +16,9 @@ pub const NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
pub const LIGHT_LEVELS: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
/// The lit world and the post pass's output.
|
||||
pub const HDR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
+/// The water pass's targets: the surface colour with the block light, and the normal with the sky light.
|
||||
+pub const WATER_TINT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
+pub const WATER_NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
/// The most lights drawn a frame.
|
||||
pub const MAX_LIGHTS: usize = 512;
|
||||
/// The side (pixels) of a tile of the screen with its own short list of lights.
|
||||
@@ -91,6 +94,13 @@ struct HdFrame {
|
||||
taa: [f32; 4],
|
||||
/// x: the history's weight; y: the sharpening (0 without TAA); zw: the jitter in world pixels.
|
||||
taa_params: [f32; 4],
|
||||
+ /// The (jittered) world-to-clip matrix the world was drawn with (rays in the water pass).
|
||||
+ view_proj: [[f32; 4]; 4],
|
||||
+ /// x: the water effect runs this frame; y: seconds for the waves (wraps at 600); zw: the
|
||||
+ /// camera's x and z modulo 256, the waves' period.
|
||||
+ water: [f32; 4],
|
||||
+ /// rgb: the sky's colour; w: 1 when there is a sky.
|
||||
+ sky_color: [f32; 4],
|
||||
}
|
||||
|
||||
/// What the world around looks like this frame (from the renderer's globals).
|
||||
@@ -116,6 +126,11 @@ pub struct HdScene {
|
||||
pub fog: [f32; 4],
|
||||
/// The sky has a sun at all (the Overworld).
|
||||
pub sky: bool,
|
||||
+ pub sky_color: [f32; 3],
|
||||
+ /// The camera's x and z modulo 256 (the waves' period), so the waves hold still in the world.
|
||||
+ pub camera_mod: [f32; 2],
|
||||
+ /// The water effect may run (not underwater, not disabled).
|
||||
+ pub water: bool,
|
||||
}
|
||||
|
||||
/// The cascades' half sizes (blocks) and shadow map size.
|
||||
@@ -137,6 +152,9 @@ struct HdFrame {
|
||||
prev_view_proj: mat4x4<f32>,
|
||||
taa: vec4<f32>,
|
||||
taa_params: vec4<f32>,
|
||||
+ view_proj: mat4x4<f32>,
|
||||
+ water: vec4<f32>,
|
||||
+ sky_color: vec4<f32>,
|
||||
}
|
||||
struct FullOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@@ -175,6 +193,8 @@ struct Light {
|
||||
@group(0) @binding(12) var<storage, read> tiles: array<u32>;
|
||||
// Ambient occlusion at half resolution: x the occlusion (1 open), y the distance it was taken at.
|
||||
@group(0) @binding(13) var ao_tex: texture_2d<f32>;
|
||||
+// The water pass's depth: the nearest water surface (0 where there is none).
|
||||
+@group(0) @binding(14) var water_depth: texture_depth_2d;
|
||||
|
||||
// The occlusion at a full-resolution pixel `dist` away: the four nearest half-resolution texels
|
||||
// weighted by closeness and by how near their distance is, so it doesn't bleed across edges.
|
||||
@@ -216,6 +236,12 @@ fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
|
||||
|
||||
// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow; soft-edged.
|
||||
fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
||||
+ return shadow_taps(p, n, 1);
|
||||
+}
|
||||
+
|
||||
+// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and
|
||||
+// blurs it anyway).
|
||||
+fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
|
||||
let dist = length(p);
|
||||
let c = select(1, 0, dist < frame.shadow.y * 0.92);
|
||||
let texel = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
|
||||
@@ -224,13 +250,13 @@ fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
||||
if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
|
||||
var lit = 0.0;
|
||||
let step = 1.0 / 2048.0;
|
||||
- for (var y = -1; y <= 1; y++) {
|
||||
- for (var x = -1; x <= 1; x++) {
|
||||
+ for (var y = -r; y <= r; y++) {
|
||||
+ for (var x = -r; x <= r; x++) {
|
||||
lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
|
||||
}
|
||||
}
|
||||
let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
||||
- return mix(1.0, lit / 9.0, fade);
|
||||
+ return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
|
||||
}
|
||||
|
||||
// One shadow sample, for the rays through the air.
|
||||
@@ -252,7 +278,15 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let scene = textureLoad(scene_color, px, 0).rgb;
|
||||
let d = textureLoad(scene_depth, px, 0);
|
||||
let g = textureLoad(gbuffer_depth, px, 0);
|
||||
- let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(d, 0.000001), 1.0);
|
||||
+ // Water in front of the opaque world with nothing translucent over it: here the floor is lit
|
||||
+ // and the water composite lays the surface over it (it knows the pixel by the alpha 0.5).
|
||||
+ var water = false;
|
||||
+ if frame.water.x > 0.5 && g > 0.0 {
|
||||
+ let wd = textureLoad(water_depth, px, 0);
|
||||
+ water = wd > 0.0 && wd >= g && d - wd <= d * 0.002;
|
||||
+ }
|
||||
+ let dd = select(d, g, water);
|
||||
+ let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(dd, 0.000001), 1.0);
|
||||
let h = frame.inverse_view_proj * ndc;
|
||||
let p = h.xyz / h.w;
|
||||
let view = normalize(p);
|
||||
@@ -267,7 +301,7 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let halo_all = tiles[tile + 3u] == 0xffffffffu;
|
||||
let halo_count = select(tiles[tile + 3u], count, halo_all);
|
||||
var c = scene;
|
||||
- let surface = d > 0.0 && abs(d - g) <= max(d, g) * 0.002;
|
||||
+ let surface = water || (d > 0.0 && abs(d - g) <= max(d, g) * 0.002);
|
||||
if surface {
|
||||
let n = normalize(textureLoad(normal_tex, px, 0).xyz);
|
||||
let albedo = textureLoad(albedo_tex, px, 0);
|
||||
@@ -278,14 +312,15 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let sky_reach = smoothstep(0.35, 0.95, lm.x);
|
||||
var s = 1.0;
|
||||
let ndl = dot(n, sun);
|
||||
- if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow(p, n); } else { s = 0.0; }
|
||||
+ if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow_taps(p, n, select(1, 0, water)); } else { s = 0.0; }
|
||||
// Shadowed sky light (ambient) keeps vanilla's look, blocks' light stays as is; the sun adds
|
||||
// direct light by the face's angle.
|
||||
let direct = frame.sun_color.rgb * sun_on * max(ndl, 0.0) * s * sky_reach;
|
||||
// Contact shadows from the ambient occlusion: on the ambient and blocks' light fully, on
|
||||
// the mods' lights by half, not on the sun or on emission.
|
||||
var occlusion = 1.0;
|
||||
- if frame.ambient.w > 0.0 { occlusion = ao_at(px, length(p)); }
|
||||
+ // (The water dims and blurs the floor under it: no occlusion there.)
|
||||
+ if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); }
|
||||
let ao_ambient = mix(1.0, occlusion, frame.ambient.w);
|
||||
let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5);
|
||||
var light = max(base * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
|
||||
@@ -334,7 +369,253 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let a1 = atan((far - t0) / hh);
|
||||
c += l.color.rgb * l.color.w * (a1 - a0) / hh * 0.004 * (0.3 + density * 300.0);
|
||||
}
|
||||
- return vec4<f32>(c, 1.0);
|
||||
+ return vec4<f32>(c, select(1.0, 0.5, water));
|
||||
+}
|
||||
+"#;
|
||||
+
|
||||
+const WATER: &str = r#"
|
||||
+const PI: f32 = 3.14159265;
|
||||
+const REFLECT_STEPS: i32 = 24;
|
||||
+const REFLECT_RANGE: f32 = 96.0;
|
||||
+@group(0) @binding(0) var<uniform> frame: HdFrame;
|
||||
+@group(0) @binding(1) var lit_tex: texture_2d<f32>;
|
||||
+@group(0) @binding(2) var gbuffer_depth: texture_depth_2d;
|
||||
+@group(0) @binding(3) var water_tint: texture_2d<f32>;
|
||||
+@group(0) @binding(4) var water_normal: texture_2d<f32>;
|
||||
+@group(0) @binding(5) var water_depth: texture_depth_2d;
|
||||
+@group(0) @binding(6) var shadow_map: texture_depth_2d_array;
|
||||
+@group(0) @binding(7) var shadow_sampler: sampler_comparison;
|
||||
+@group(0) @binding(8) var lightmap: texture_2d<f32>;
|
||||
+@group(0) @binding(9) var linear: sampler;
|
||||
+
|
||||
+fn ign(px: vec2<f32>) -> f32 {
|
||||
+ let p = px + vec2<f32>(5.588238 * frame.params.w);
|
||||
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
|
||||
+}
|
||||
+
|
||||
+// The camera-relative position of the screen point `uv` at depth z.
|
||||
+fn position_at(uv: vec2<f32>, z: f32) -> vec3<f32> {
|
||||
+ let h = frame.inverse_view_proj * vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
|
||||
+ return h.xyz / h.w;
|
||||
+}
|
||||
+
|
||||
+// The screen point (uv, clip w) of a camera-relative position.
|
||||
+fn project(p: vec3<f32>) -> vec3<f32> {
|
||||
+ let c = frame.view_proj * vec4<f32>(p, 1.0);
|
||||
+ return vec3<f32>(c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5, c.w);
|
||||
+}
|
||||
+
|
||||
+fn on_screen(uv: vec2<f32>) -> bool {
|
||||
+ return all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0));
|
||||
+}
|
||||
+
|
||||
+fn texel(uv: vec2<f32>) -> vec2<i32> {
|
||||
+ return clamp(vec2<i32>(uv * frame.resolution.xy), vec2<i32>(0), vec2<i32>(frame.resolution.xy) - 1);
|
||||
+}
|
||||
+
|
||||
+// Sine waves in world space: wave vectors that are whole multiples of 2 pi / 256 and speeds that
|
||||
+// are whole cycles per 600 s, so the sum repeats every 256 blocks and 10 minutes and holds still
|
||||
+// in the world as the camera moves. x, y: the vector in units of 2 pi / 256; z: the slope the
|
||||
+// wave adds; w: its phase.
|
||||
+const WAVES = array<vec4<f32>, 10>(
|
||||
+ vec4<f32>(3.0, 1.0, 0.040, 0.3),
|
||||
+ vec4<f32>(-2.0, 5.0, 0.045, 2.1),
|
||||
+ vec4<f32>(7.0, 2.0, 0.040, 4.4),
|
||||
+ vec4<f32>(-5.0, -9.0, 0.040, 1.2),
|
||||
+ vec4<f32>(11.0, -4.0, 0.035, 5.6),
|
||||
+ vec4<f32>(13.0, 10.0, 0.035, 3.3),
|
||||
+ vec4<f32>(-17.0, 7.0, 0.030, 0.9),
|
||||
+ vec4<f32>(9.0, 23.0, 0.030, 2.7),
|
||||
+ vec4<f32>(-27.0, 19.0, 0.025, 4.9),
|
||||
+ vec4<f32>(31.0, 30.0, 0.025, 1.8),
|
||||
+);
|
||||
+
|
||||
+// The gradient of the water's height at world x, z: the waves that a pixel `footprint` blocks
|
||||
+// wide can still resolve (finer ones fade out, or they would shimmer).
|
||||
+fn wave_gradient(xz: vec2<f32>, footprint: f32) -> vec2<f32> {
|
||||
+ var grad = vec2<f32>(0.0);
|
||||
+ for (var i = 0; i < 10; i++) {
|
||||
+ let w = WAVES[i];
|
||||
+ let k = w.xy * (2.0 * PI / 256.0);
|
||||
+ let len = length(k);
|
||||
+ let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.25 - 0.5, 0.0, 1.0);
|
||||
+ let cycles = round(sqrt(9.8 * len) * 0.5 * 600.0 / (2.0 * PI));
|
||||
+ let omega = cycles * 2.0 * PI / 600.0;
|
||||
+ let phase = dot(k, xz) - omega * frame.water.y + w.w;
|
||||
+ grad += (k / len) * (w.z * fade * cos(phase));
|
||||
+ }
|
||||
+ return grad;
|
||||
+}
|
||||
+
|
||||
+fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
|
||||
+ let s = frame.cascades[c] * vec4<f32>(p, 1.0);
|
||||
+ return vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
|
||||
+}
|
||||
+
|
||||
+// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow (as the lighting pass has it).
|
||||
+fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
||||
+ let dist = length(p);
|
||||
+ let c = select(1, 0, dist < frame.shadow.y * 0.92);
|
||||
+ let texel_size = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
|
||||
+ let q = p + n * texel_size * 1.5;
|
||||
+ let s = cascade_at(q, c);
|
||||
+ if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
|
||||
+ var lit = 0.0;
|
||||
+ let step = 1.0 / 2048.0;
|
||||
+ for (var y = -1; y <= 1; y++) {
|
||||
+ for (var x = -1; x <= 1; x++) {
|
||||
+ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
|
||||
+ }
|
||||
+ }
|
||||
+ let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
||||
+ return mix(1.0, lit / 9.0, fade);
|
||||
+}
|
||||
+
|
||||
+fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
|
||||
+ if d <= start { return 0.0; }
|
||||
+ if d >= end { return 1.0; }
|
||||
+ return (d - start) / (end - start);
|
||||
+}
|
||||
+
|
||||
+// What the sky shows along r when the ray leaves the screen: the horizon's colour rising to
|
||||
+// the zenith's, darker where the water lies under cover (`vis` is the sky light there).
|
||||
+fn sky_along(r: vec3<f32>, vis: f32) -> vec3<f32> {
|
||||
+ var c = frame.fog_color.rgb;
|
||||
+ if frame.sky_color.w > 0.5 {
|
||||
+ c = mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, r.y));
|
||||
+ }
|
||||
+ return c * mix(0.12, 1.0, vis);
|
||||
+}
|
||||
+
|
||||
+// Marches the ray from p along r over the depth buffer (28 steps, growing, jittered so TAA
|
||||
+// averages the placement; then a binary search for the crossing). xyz: the colour found,
|
||||
+// w: how much to trust it (0 for a miss, less near the screen's edge).
|
||||
+fn reflection(p: vec3<f32>, r: vec3<f32>, jitter: f32) -> vec4<f32> {
|
||||
+ var near = 0.0;
|
||||
+ for (var i = 0; i < REFLECT_STEPS; i++) {
|
||||
+ let u = (f32(i) + jitter) / f32(REFLECT_STEPS);
|
||||
+ let s = 0.12 + REFLECT_RANGE * u * u;
|
||||
+ let q = p + r * s;
|
||||
+ let sp = project(q);
|
||||
+ if sp.z <= 0.0 || !on_screen(sp.xy) { break; }
|
||||
+ let z = textureLoad(gbuffer_depth, texel(sp.xy), 0);
|
||||
+ if z > 0.0 {
|
||||
+ let c = frame.view_proj * vec4<f32>(q, 1.0);
|
||||
+ // Reversed Z: a larger depth is nearer; the ray is behind the surface when the
|
||||
+ // surface is nearer than it.
|
||||
+ if z > c.z / c.w {
|
||||
+ let behind = length(q) - length(position_at(sp.xy, z));
|
||||
+ if behind < 0.4 + 0.08 * s {
|
||||
+ var lo = near;
|
||||
+ var hi = s;
|
||||
+ for (var k = 0; k < 5; k++) {
|
||||
+ let m = 0.5 * (lo + hi);
|
||||
+ let mq = p + r * m;
|
||||
+ let mp = project(mq);
|
||||
+ let mc = frame.view_proj * vec4<f32>(mq, 1.0);
|
||||
+ let mz = textureLoad(gbuffer_depth, texel(mp.xy), 0);
|
||||
+ if mz > mc.z / mc.w { hi = m; } else { lo = m; }
|
||||
+ }
|
||||
+ let hp = project(p + r * hi);
|
||||
+ let edge = min(min(hp.x, 1.0 - hp.x), min(hp.y, 1.0 - hp.y));
|
||||
+ let colour = textureSampleLevel(lit_tex, linear, hp.xy, 0.0).rgb;
|
||||
+ return vec4<f32>(colour, smoothstep(0.0, 0.08, edge));
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ near = s;
|
||||
+ }
|
||||
+ return vec4<f32>(0.0);
|
||||
+}
|
||||
+
|
||||
+// The sun's glint on the waves (GGX, a rough lobe so it glitters instead of aliasing).
|
||||
+fn glint(n: vec3<f32>, v: vec3<f32>, l: vec3<f32>) -> f32 {
|
||||
+ let h = normalize(l + v);
|
||||
+ let a = 0.05;
|
||||
+ let nh = max(dot(n, h), 0.0);
|
||||
+ let d = a * a / (PI * pow(nh * nh * (a * a - 1.0) + 1.0, 2.0));
|
||||
+ let f = 0.02 + 0.98 * pow(1.0 - max(dot(v, h), 0.0), 5.0);
|
||||
+ return min(d * f * 0.25 * max(dot(n, l), 0.0) / max(dot(n, v), 0.1), 12.0);
|
||||
+}
|
||||
+
|
||||
+// The water over the lit world: the world behind it (refracted by the waves, dimmed and tinted
|
||||
+// by the water's depth), the sky and the world above it (a screen-space reflection) by Fresnel,
|
||||
+// and the sun's glint. Pixels without water pass through.
|
||||
+@fragment
|
||||
+fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
||||
+ let px = vec2<i32>(in.position.xy);
|
||||
+ let base = textureLoad(lit_tex, px, 0);
|
||||
+ if base.a > 0.75 { return vec4<f32>(base.rgb, 1.0); }
|
||||
+ let wd = textureLoad(water_depth, px, 0);
|
||||
+ let g = textureLoad(gbuffer_depth, px, 0);
|
||||
+ let tint = textureLoad(water_tint, px, 0);
|
||||
+ let nt = textureLoad(water_normal, px, 0);
|
||||
+ let p = position_at(in.uv, wd);
|
||||
+ let dist = length(p);
|
||||
+ let v = -p / dist;
|
||||
+ let geometric = normalize(nt.xyz * 2.0 - 1.0);
|
||||
+ // The waves tilt the flat parts of the surface only.
|
||||
+ let footprint = dist * frame.resolution.w * 1.4;
|
||||
+ let grad = wave_gradient(p.xz + frame.water.zw, footprint);
|
||||
+ let flat_part = smoothstep(0.6, 0.95, geometric.y);
|
||||
+ var n = normalize(geometric + vec3<f32>(-grad.x, 0.0, -grad.y) * flat_part);
|
||||
+ n = normalize(n + v * max(0.03 - dot(n, v), 0.0));
|
||||
+ let ndv = max(dot(n, v), 0.02);
|
||||
+
|
||||
+ // Light on the water: vanilla's lightmap at the surface, and the sun where it reaches.
|
||||
+ let levels = vec2<f32>(nt.a, tint.a);
|
||||
+ let sky_reach = smoothstep(0.35, 0.95, levels.x);
|
||||
+ let ambient = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
|
||||
+ let block = textureSampleLevel(lightmap, linear, vec2<f32>(levels.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
|
||||
+ let sun = frame.sun.xyz;
|
||||
+ let sun_on = frame.sun.w;
|
||||
+ var sunlit = 0.0;
|
||||
+ if sun_on > 0.0 && sun.y > 0.0 && sky_reach > 0.0 { sunlit = shadow(p, vec3<f32>(0.0, 1.0, 0.0)) * sky_reach; }
|
||||
+ let volume_light = max(ambient * frame.ambient.x, block * frame.ambient.y) + frame.sun_color.rgb * sun_on * sunlit * 0.8;
|
||||
+
|
||||
+ // Refraction: where the ray through the waves meets the world, against where the flat surface
|
||||
+ // would send it (so still water looks as vanilla's and only the waves distort).
|
||||
+ let floor0 = position_at(in.uv, g);
|
||||
+ let depth0 = max(length(floor0) - dist, 0.0);
|
||||
+ let bent = refract(-v, n, 0.75) - refract(-v, geometric, 0.75);
|
||||
+ var uv_floor = in.uv;
|
||||
+ var floor_z = g;
|
||||
+ let moved = project(floor0 + bent * min(depth0, 24.0));
|
||||
+ if moved.z > 0.0 && on_screen(moved.xy) {
|
||||
+ let z = textureLoad(gbuffer_depth, texel(moved.xy), 0);
|
||||
+ // Only what lies behind the water: not the bank or an entity in front of it.
|
||||
+ if z > 0.0 && z <= wd {
|
||||
+ uv_floor = moved.xy;
|
||||
+ floor_z = z;
|
||||
+ }
|
||||
+ }
|
||||
+ let thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0);
|
||||
+ let behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb;
|
||||
+ let colour = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
|
||||
+ // The colours the water takes up fastest are the ones it has the least of.
|
||||
+ let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.8 * (thickness + 0.3));
|
||||
+ let refracted = behind * absorb + colour * volume_light * 0.9 * (vec3<f32>(1.0) - absorb);
|
||||
+
|
||||
+ // Reflection: the screen where the ray stays on it, the sky where it leaves it or misses.
|
||||
+ var r = reflect(-v, n);
|
||||
+ r.y = abs(r.y);
|
||||
+ // Looking down onto it the reflection is a few percent of the picture: the sky alone does.
|
||||
+ let fresnel = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);
|
||||
+ var hit = vec4<f32>(0.0);
|
||||
+ if fresnel > 0.04 { hit = reflection(p, r, ign(in.position.xy)); }
|
||||
+ let sky_vis = mix(0.15, 1.0, sky_reach);
|
||||
+ let reflected = mix(sky_along(r, sky_vis), hit.rgb, hit.w);
|
||||
+
|
||||
+ var c = refracted * (1.0 - fresnel) + reflected * fresnel;
|
||||
+ if sun_on > 0.0 && sunlit > 0.0 {
|
||||
+ c += frame.sun_color.rgb * sun_on * sunlit * glint(n, v, sun);
|
||||
+ }
|
||||
+ // Vanilla's fog over it, at the surface.
|
||||
+ let sph = dist;
|
||||
+ let cyl = max(length(p.xz), abs(p.y));
|
||||
+ let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
|
||||
+ c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a);
|
||||
+ return vec4<f32>(max(c, vec3<f32>(0.0)), 1.0);
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -829,6 +1110,12 @@ pub fn taa_enabled() -> bool {
|
||||
*ON.get_or_init(|| std::env::var("HORIZON_HD_TAA").map_or(true, |v| v != "0"))
|
||||
}
|
||||
|
||||
+/// The water effect is on unless `HORIZON_HD_WATER=0`.
|
||||
+pub fn water_enabled() -> bool {
|
||||
+ static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
+ *ON.get_or_init(|| std::env::var("HORIZON_HD_WATER").map_or(true, |v| v != "0"))
|
||||
+}
|
||||
+
|
||||
/// The Halton sequence's `index`-th number in `base`, in (0, 1).
|
||||
fn halton(mut index: u32, base: u32) -> f32 {
|
||||
let (mut f, mut r) = (1.0, 0.0);
|
||||
@@ -895,6 +1182,22 @@ pub struct Hd {
|
||||
/// The lit world, and the post pass's output.
|
||||
pub lit: wgpu::TextureView,
|
||||
pub fx: wgpu::TextureView,
|
||||
+ /// Water: its surface colour and normal, its depth (the nearest water surface; 0 elsewhere),
|
||||
+ /// the lit world with the water over it, and what draws and composites them.
|
||||
+ water_tint: wgpu::TextureView,
|
||||
+ water_normal: wgpu::TextureView,
|
||||
+ water_depth: wgpu::TextureView,
|
||||
+ lit_water: wgpu::TextureView,
|
||||
+ water_layout: wgpu::BindGroupLayout,
|
||||
+ water_pipeline: wgpu::RenderPipeline,
|
||||
+ water_group: Option<((wgpu::TextureView, wgpu::Sampler), wgpu::BindGroup)>,
|
||||
+ /// The water effect runs this frame (the scene allows it and there is translucent terrain on
|
||||
+ /// screen), and `lit_view` is the composite.
|
||||
+ water_active: bool,
|
||||
+ /// The frame's translucent terrain exists (set by the renderer before `write`).
|
||||
+ pub water_geometry: bool,
|
||||
+ water_composited: bool,
|
||||
+ started: std::time::Instant,
|
||||
/// TAA: the resolved frame, and the two histories (one read, one written, alternating).
|
||||
taa: wgpu::TextureView,
|
||||
history: [wgpu::TextureView; 2],
|
||||
@@ -1216,6 +1519,26 @@ impl Hd {
|
||||
min_binding_size: None,
|
||||
},
|
||||
UNFILTERED,
|
||||
+ DEPTH,
|
||||
+ ]),
|
||||
+ });
|
||||
+ let water_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
+ label: Some("HD water"),
|
||||
+ entries: &entries(&[
|
||||
+ UNIFORM,
|
||||
+ FLOAT,
|
||||
+ DEPTH,
|
||||
+ UNFILTERED,
|
||||
+ UNFILTERED,
|
||||
+ DEPTH,
|
||||
+ wgpu::BindingType::Texture {
|
||||
+ sample_type: wgpu::TextureSampleType::Depth,
|
||||
+ view_dimension: wgpu::TextureViewDimension::D2Array,
|
||||
+ multisampled: false,
|
||||
+ },
|
||||
+ wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
|
||||
+ FLOAT,
|
||||
+ SAMPLER,
|
||||
]),
|
||||
});
|
||||
let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
@@ -1278,6 +1601,16 @@ impl Hd {
|
||||
let tone = module(device, "HD tone mapping", TONEMAP);
|
||||
let ao = module(device, "HD ambient occlusion", AO);
|
||||
let ao_blur = module(device, "HD ambient occlusion blur", AO_BLUR);
|
||||
+ let water = module(device, "HD water", WATER);
|
||||
+ let water_pipeline = fullscreen(
|
||||
+ device,
|
||||
+ "HD water",
|
||||
+ &water_layout,
|
||||
+ &water,
|
||||
+ "fs_water",
|
||||
+ HDR,
|
||||
+ None,
|
||||
+ );
|
||||
let ao_pipeline = fullscreen(
|
||||
device,
|
||||
"HD ambient occlusion",
|
||||
@@ -1436,6 +1769,23 @@ impl Hd {
|
||||
.view,
|
||||
lit: target(device, "HD lit", [1, 1], HDR).view,
|
||||
fx: target(device, "HD post", [1, 1], HDR).view,
|
||||
+ water_tint: target(device, "HD water colour", [1, 1], WATER_TINT).view,
|
||||
+ water_normal: target(device, "HD water normal", [1, 1], WATER_NORMAL).view,
|
||||
+ water_depth: target(
|
||||
+ device,
|
||||
+ "HD water depth",
|
||||
+ [1, 1],
|
||||
+ wgpu::TextureFormat::Depth32Float,
|
||||
+ )
|
||||
+ .view,
|
||||
+ lit_water: target(device, "HD lit with water", [1, 1], HDR).view,
|
||||
+ water_layout,
|
||||
+ water_pipeline,
|
||||
+ water_group: None,
|
||||
+ water_active: false,
|
||||
+ water_geometry: false,
|
||||
+ water_composited: false,
|
||||
+ started: std::time::Instant::now(),
|
||||
taa: target(device, "HD TAA", [1, 1], HDR).view,
|
||||
history: [
|
||||
target(device, "HD TAA history", [1, 1], HDR).view,
|
||||
@@ -1502,6 +1852,17 @@ impl Hd {
|
||||
self.depth = target(device, "HD depth", size, wgpu::TextureFormat::Depth32Float).view;
|
||||
self.lit = target(device, "HD lit", size, HDR).view;
|
||||
self.fx = target(device, "HD post", size, HDR).view;
|
||||
+ self.water_tint = target(device, "HD water colour", size, WATER_TINT).view;
|
||||
+ self.water_normal = target(device, "HD water normal", size, WATER_NORMAL).view;
|
||||
+ self.water_depth = target(
|
||||
+ device,
|
||||
+ "HD water depth",
|
||||
+ size,
|
||||
+ wgpu::TextureFormat::Depth32Float,
|
||||
+ )
|
||||
+ .view;
|
||||
+ self.lit_water = target(device, "HD lit with water", size, HDR).view;
|
||||
+ self.water_group = None;
|
||||
self.taa = target(device, "HD TAA", out, HDR).view;
|
||||
self.history = [
|
||||
target(device, "HD TAA history", out, HDR).view,
|
||||
@@ -1591,6 +1952,7 @@ impl Hd {
|
||||
let (dir, strength, color) = light_of(scene);
|
||||
let cascades = scene.cascades;
|
||||
let night = scene.dark.clamp(0.0, 1.0);
|
||||
+ self.water_active = scene.water && water_enabled() && self.water_geometry;
|
||||
self.taa_active = scene.taa && taa_enabled();
|
||||
// A long jump of the camera (a teleport) starts the history over.
|
||||
if !self.taa_active || scene.camera_delta.length() > 64.0 {
|
||||
@@ -1604,6 +1966,19 @@ impl Hd {
|
||||
scene.camera_delta.z,
|
||||
if self.history_ready { 1.0 } else { 0.0 },
|
||||
],
|
||||
+ view_proj: scene.view_proj.to_cols_array_2d(),
|
||||
+ water: [
|
||||
+ if self.water_active { 1.0 } else { 0.0 },
|
||||
+ (self.started.elapsed().as_secs_f64() % 600.0) as f32,
|
||||
+ scene.camera_mod[0],
|
||||
+ scene.camera_mod[1],
|
||||
+ ],
|
||||
+ sky_color: [
|
||||
+ scene.sky_color[0],
|
||||
+ scene.sky_color[1],
|
||||
+ scene.sky_color[2],
|
||||
+ if scene.sky { 1.0 } else { 0.0 },
|
||||
+ ],
|
||||
taa_params: [
|
||||
if self.upscaling() {
|
||||
UPSCALE_HISTORY
|
||||
@@ -1821,6 +2196,10 @@ impl Hd {
|
||||
binding: 13,
|
||||
resource: wgpu::BindingResource::TextureView(&self.ao_blurred),
|
||||
},
|
||||
+ wgpu::BindGroupEntry {
|
||||
+ binding: 14,
|
||||
+ resource: wgpu::BindingResource::TextureView(&self.water_depth),
|
||||
+ },
|
||||
],
|
||||
});
|
||||
self.light_group = Some((key, group));
|
||||
@@ -1828,6 +2207,7 @@ impl Hd {
|
||||
let Some((_, group)) = &self.light_group else {
|
||||
return;
|
||||
};
|
||||
+ self.water_composited = false;
|
||||
Self::draw(
|
||||
encoder,
|
||||
"HD lighting",
|
||||
@@ -1894,7 +2274,7 @@ impl Hd {
|
||||
screen: &wgpu::TextureView,
|
||||
) {
|
||||
let source = match source {
|
||||
- HdSource::Lit => self.lit.clone(),
|
||||
+ HdSource::Lit => self.lit_view().clone(),
|
||||
HdSource::Fx => self.fx.clone(),
|
||||
HdSource::Taa => self.taa.clone(),
|
||||
};
|
||||
@@ -1993,7 +2373,7 @@ impl Hd {
|
||||
return source;
|
||||
}
|
||||
let input = match source {
|
||||
- HdSource::Lit => self.lit.clone(),
|
||||
+ HdSource::Lit => self.lit_view().clone(),
|
||||
HdSource::Fx => self.fx.clone(),
|
||||
HdSource::Taa => return source,
|
||||
};
|
||||
@@ -2052,6 +2432,102 @@ impl Hd {
|
||||
HdSource::Taa
|
||||
}
|
||||
|
||||
+ /// The lit world as later passes see it: with the water over it when the effect ran.
|
||||
+ pub fn lit_view(&self) -> &wgpu::TextureView {
|
||||
+ if self.water_composited {
|
||||
+ &self.lit_water
|
||||
+ } else {
|
||||
+ &self.lit
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// The water effect runs this frame (the renderer draws the water quads with `water_pass`).
|
||||
+ pub fn water_active(&self) -> bool {
|
||||
+ self.water_active
|
||||
+ }
|
||||
+
|
||||
+ /// The water pass's attachments: the surface colour and normal, and the water's own depth,
|
||||
+ /// all cleared. The caller draws the translucent terrain with `fs_water`.
|
||||
+ pub fn water_pass<'e>(
|
||||
+ &mut self,
|
||||
+ encoder: &'e mut wgpu::CommandEncoder,
|
||||
+ ) -> wgpu::RenderPass<'e> {
|
||||
+ let clear = |view| {
|
||||
+ Some(wgpu::RenderPassColorAttachment {
|
||||
+ view,
|
||||
+ resolve_target: None,
|
||||
+ depth_slice: None,
|
||||
+ ops: wgpu::Operations {
|
||||
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
+ store: wgpu::StoreOp::Store,
|
||||
+ },
|
||||
+ })
|
||||
+ };
|
||||
+ encoder
|
||||
+ .begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
+ label: Some("HD water"),
|
||||
+ timestamp_writes: stamp(&mut self.timing, "hd water"),
|
||||
+ color_attachments: &[clear(&self.water_tint), clear(&self.water_normal)],
|
||||
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
+ view: &self.water_depth,
|
||||
+ depth_ops: Some(wgpu::Operations {
|
||||
+ load: wgpu::LoadOp::Clear(0.0),
|
||||
+ store: wgpu::StoreOp::Store,
|
||||
+ }),
|
||||
+ stencil_ops: None,
|
||||
+ }),
|
||||
+ ..Default::default()
|
||||
+ })
|
||||
+ .forget_lifetime()
|
||||
+ }
|
||||
+
|
||||
+ /// Lays the water over the lit world (refraction, reflection, glint) into a target of its
|
||||
+ /// own; `lit_view` is that target after this.
|
||||
+ pub fn water_composite(
|
||||
+ &mut self,
|
||||
+ device: &wgpu::Device,
|
||||
+ encoder: &mut wgpu::CommandEncoder,
|
||||
+ lightmap: (&wgpu::TextureView, &wgpu::Sampler),
|
||||
+ ) {
|
||||
+ if !self.water_active {
|
||||
+ return;
|
||||
+ }
|
||||
+ let key = (lightmap.0.clone(), lightmap.1.clone());
|
||||
+ if self.water_group.as_ref().is_none_or(|(k, _)| *k != key) {
|
||||
+ let group = self.make_group(
|
||||
+ device,
|
||||
+ "HD water",
|
||||
+ &self.water_layout,
|
||||
+ &[
|
||||
+ wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
|
||||
+ wgpu::BindingResource::TextureView(&self.lit),
|
||||
+ wgpu::BindingResource::TextureView(&self.depth),
|
||||
+ wgpu::BindingResource::TextureView(&self.water_tint),
|
||||
+ wgpu::BindingResource::TextureView(&self.water_normal),
|
||||
+ wgpu::BindingResource::TextureView(&self.water_depth),
|
||||
+ wgpu::BindingResource::TextureView(&self.shadow_array),
|
||||
+ wgpu::BindingResource::Sampler(&self.shadow_sampler),
|
||||
+ wgpu::BindingResource::TextureView(lightmap.0),
|
||||
+ wgpu::BindingResource::Sampler(&self.sampler),
|
||||
+ ],
|
||||
+ );
|
||||
+ self.water_group = Some((key, group));
|
||||
+ }
|
||||
+ let Some((_, group)) = &self.water_group else {
|
||||
+ return;
|
||||
+ };
|
||||
+ Self::draw(
|
||||
+ encoder,
|
||||
+ "HD water composite",
|
||||
+ &self.lit_water,
|
||||
+ false,
|
||||
+ &self.water_pipeline,
|
||||
+ group,
|
||||
+ stamp(&mut self.timing, "hd water composite"),
|
||||
+ );
|
||||
+ self.water_composited = true;
|
||||
+ }
|
||||
+
|
||||
/// The ambient occlusion from the G-buffer's depth and normals: the half-resolution pass, then
|
||||
/// its blur. Run after the G-buffer pass; nothing when the strength is 0.
|
||||
pub fn ao(&mut self, encoder: &mut wgpu::CommandEncoder) {
|
||||
@@ -2181,6 +2657,7 @@ mod tests {
|
||||
("taa", TAA),
|
||||
("ao", AO),
|
||||
("ao blur", AO_BLUR),
|
||||
+ ("water", WATER),
|
||||
] {
|
||||
let source = format!("{COMMON}{body}");
|
||||
let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| {
|
||||
diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl
|
||||
index c44449cc7..0d5d2fc89 100644
|
||||
--- a/crates/horizon-client/src/terrain.wgsl
|
||||
+++ b/crates/horizon-client/src/terrain.wgsl
|
||||
@@ -182,6 +182,25 @@ fn face_normal(pos: vec3<f32>) -> vec3<f32> {
|
||||
out.light = vec4(clamp(in.lm, vec2(0.0), vec2(1.0)), 0.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
+// AirDef fork, HD water: the water quads once more, after the G-buffer, into small targets of
|
||||
+// their own: the surface colour (the water sprite times the biome tint) with the block light, and
|
||||
+// the face normal with the sky light. The layers are the water sprites' (gfx.rs fills them in;
|
||||
+// nothing else is water: glass, ice and the rest stay as vanilla draws them).
|
||||
+override WATER_STILL: u32 = 0xffffu;
|
||||
+override WATER_FLOW: u32 = 0xffffu;
|
||||
+struct WaterOut {
|
||||
+ @location(0) tint: vec4<f32>,
|
||||
+ @location(1) normal: vec4<f32>,
|
||||
+}
|
||||
+@fragment fn fs_water(in: TerrainOut) -> WaterOut {
|
||||
+ let layer = in.layer & 0x7fffu;
|
||||
+ if layer != WATER_STILL && layer != WATER_FLOW { discard; }
|
||||
+ let color = sprite(in.uv, layer) * in.tint;
|
||||
+ var out: WaterOut;
|
||||
+ out.tint = vec4(color.rgb, clamp(in.lm.y, 0.0, 1.0));
|
||||
+ out.normal = vec4(face_normal(in.pos) * 0.5 + 0.5, clamp(in.lm.x, 0.0, 1.0));
|
||||
+ return out;
|
||||
+}
|
||||
// AirDef fork: the sun's shadow map — depth only, cut-out leaves leave holes.
|
||||
@fragment fn fs_shadow(in: TerrainOut) {
|
||||
let a = sprite(in.uv, in.layer & 0x7fffu).a;
|
||||
--
|
||||
2.54.0 (Apple Git-157)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
From dbc6f8a6e9c2eeabe1de15f1794013c90be7faa5 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:56:04 +0300
|
||||
Subject: [PATCH 20/22] HD water: no vanilla texture on the surface (the biome
|
||||
colour only), a darker, deeper colour
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
crates/horizon-client/src/gfx/hd.rs | 9 ++++++---
|
||||
crates/horizon-client/src/terrain.wgsl | 6 ++++--
|
||||
2 files changed, 10 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
||||
index f5caa2d0b..8070aab3c 100644
|
||||
--- a/crates/horizon-client/src/gfx/hd.rs
|
||||
+++ b/crates/horizon-client/src/gfx/hd.rs
|
||||
@@ -591,10 +591,13 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
let thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0);
|
||||
let behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb;
|
||||
- let colour = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
|
||||
+ // A deep, dark water: the biome's colour darkened and a little greyed, so it reads as
|
||||
+ // depth rather than as bright paint.
|
||||
+ let biome = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
|
||||
+ let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.8) * vec3<f32>(0.55, 0.68, 0.8);
|
||||
// The colours the water takes up fastest are the ones it has the least of.
|
||||
- let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.8 * (thickness + 0.3));
|
||||
- let refracted = behind * absorb + colour * volume_light * 0.9 * (vec3<f32>(1.0) - absorb);
|
||||
+ let absorb = exp(-(vec3<f32>(1.0) - colour) * 1.1 * (thickness + 0.3));
|
||||
+ let refracted = behind * absorb + colour * volume_light * 0.6 * (vec3<f32>(1.0) - absorb);
|
||||
|
||||
// Reflection: the screen where the ray stays on it, the sky where it leaves it or misses.
|
||||
var r = reflect(-v, n);
|
||||
diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl
|
||||
index 0d5d2fc89..e892d5a0f 100644
|
||||
--- a/crates/horizon-client/src/terrain.wgsl
|
||||
+++ b/crates/horizon-client/src/terrain.wgsl
|
||||
@@ -195,9 +195,11 @@ struct WaterOut {
|
||||
@fragment fn fs_water(in: TerrainOut) -> WaterOut {
|
||||
let layer = in.layer & 0x7fffu;
|
||||
if layer != WATER_STILL && layer != WATER_FLOW { discard; }
|
||||
- let color = sprite(in.uv, layer) * in.tint;
|
||||
+ // The biome's colour at the texture's mean brightness, not the texture: the HD water draws
|
||||
+ // its own surface (waves, reflections), and vanilla's ripples on top would look painted on.
|
||||
+ let color = vec3(0.62) * in.tint.rgb;
|
||||
var out: WaterOut;
|
||||
- out.tint = vec4(color.rgb, clamp(in.lm.y, 0.0, 1.0));
|
||||
+ out.tint = vec4(color, clamp(in.lm.y, 0.0, 1.0));
|
||||
out.normal = vec4(face_normal(in.pos) * 0.5 + 0.5, clamp(in.lm.x, 0.0, 1.0));
|
||||
return out;
|
||||
}
|
||||
--
|
||||
2.54.0 (Apple Git-157)
|
||||
|
||||
@@ -0,0 +1,945 @@
|
||||
From 13e74537dd6e63482e3613e0e416de1158944dfe Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 18:38:19 +0300
|
||||
Subject: [PATCH 21/22] HD sky and light: a physical atmosphere (Rayleigh, Mie,
|
||||
ozone; constants and transmittance after Photon, MIT) for the sky, haze, fog
|
||||
and water reflections; warmer sunlight from the atmosphere's transmittance, a
|
||||
gentle sky-coloured ambient, soft PCSS-lite shadows near the player, a haze
|
||||
round the sun; lighter water with fine ripples
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
crates/horizon-client/src/gfx.rs | 4 +
|
||||
.../src/gfx/THIRD_PARTY_PHOTON.md | 34 ++
|
||||
crates/horizon-client/src/gfx/hd.rs | 259 +++++++++++++-
|
||||
crates/horizon-client/src/gfx/hd_sky.rs | 324 ++++++++++++++++++
|
||||
crates/horizon-client/src/terrain.wgsl | 2 +-
|
||||
5 files changed, 604 insertions(+), 19 deletions(-)
|
||||
create mode 100644 crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md
|
||||
create mode 100644 crates/horizon-client/src/gfx/hd_sky.rs
|
||||
|
||||
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
|
||||
index f9dab8abf..d01131433 100644
|
||||
--- a/crates/horizon-client/src/gfx.rs
|
||||
+++ b/crates/horizon-client/src/gfx.rs
|
||||
@@ -72,6 +72,7 @@ mod cull;
|
||||
mod end_sky;
|
||||
mod flat;
|
||||
mod hd;
|
||||
+mod hd_sky;
|
||||
mod hdr;
|
||||
pub mod lightning;
|
||||
mod lod;
|
||||
@@ -4275,6 +4276,8 @@ impl Renderer {
|
||||
camera.z.rem_euclid(256.0) as f32,
|
||||
],
|
||||
water: !frame.underwater,
|
||||
+ sunrise: frame.sky.sunrise_color,
|
||||
+ sky_fog_end: frame.fog.map_or(frame.distance.min(512.0), |f| f.sky_end),
|
||||
};
|
||||
let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene);
|
||||
let mut hd_cascades = hd::cascades(camera, shadow_dir);
|
||||
@@ -5817,6 +5820,7 @@ impl Renderer {
|
||||
{
|
||||
// AirDef fork: lit by the mods' lights into the HD target, the mod's post pass on
|
||||
// that, then bloom and the tone curve onto the screen.
|
||||
+ hd.sky(&mut encoder);
|
||||
hd.light(
|
||||
&self.device,
|
||||
&mut encoder,
|
||||
diff --git a/crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md b/crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md
|
||||
new file mode 100644
|
||||
index 000000000..03d945001
|
||||
--- /dev/null
|
||||
+++ b/crates/horizon-client/src/gfx/THIRD_PARTY_PHOTON.md
|
||||
@@ -0,0 +1,34 @@
|
||||
+# Third-party code: Photon
|
||||
+
|
||||
+Parts of the HD renderer's atmosphere (`hd_sky.rs`) are ported from, or follow closely, the
|
||||
+Photon shader pack by sixthsurge, `shaders/include/sky/atmosphere.glsl` (and the idea of the water
|
||||
+waves in `shaders/include/misc/water_normal.glsl`) at commit
|
||||
+26c2906ee02cc484359b835fba6993d443d45966, https://github.com/sixthsurge/photon
|
||||
+
|
||||
+What is taken: the air's scattering and absorption coefficients (Rayleigh, Mie, ozone), the scale
|
||||
+heights, the ozone density profile, the Mie albedo and Henyey-Greenstein anisotropy, the sunlight
|
||||
+colour outside the atmosphere, and the Chapman-function approximation of the sun's transmittance.
|
||||
+The sky-view texture, its march, the sun's colour and everything else are our own.
|
||||
+
|
||||
+Photon is distributed under the MIT License:
|
||||
+
|
||||
+ The MIT License
|
||||
+
|
||||
+ Copyright © 2025 Benjamin Stott "sixthsurge"
|
||||
+
|
||||
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
+ and associated documentation files (the "Software"), to deal in the Software without
|
||||
+ restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
+ distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
+ Software is furnished to do so, subject to the following conditions:
|
||||
+
|
||||
+ The above copyright notice and this permission notice shall be included in all copies or
|
||||
+ substantial portions of the Software.
|
||||
+
|
||||
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
+ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+
|
||||
+No other shader pack's code (BSL, Complementary and so on) may be used: their licences forbid it.
|
||||
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
||||
index 8070aab3c..031e9f2de 100644
|
||||
--- a/crates/horizon-client/src/gfx/hd.rs
|
||||
+++ b/crates/horizon-client/src/gfx/hd.rs
|
||||
@@ -5,6 +5,7 @@
|
||||
//! into a high-dynamic-range target; the mod's post pass runs on that; then
|
||||
//! bright parts bloom and a tone curve brings it to the screen — vanilla's
|
||||
//! range (up to about 0.8) left as it is, only what is brighter rolled off.
|
||||
+use super::hd_sky;
|
||||
use super::pack_timing::PassTiming;
|
||||
use glam::Mat4;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -101,6 +102,12 @@ struct HdFrame {
|
||||
water: [f32; 4],
|
||||
/// rgb: the sky's colour; w: 1 when there is a sky.
|
||||
sky_color: [f32; 4],
|
||||
+ /// xyz: towards the sun itself (not the moon); w: 1 when the atmosphere draws the sky.
|
||||
+ sky_sun: [f32; 4],
|
||||
+ /// x: vanilla's sky fog end; z: how overcast the sky is (rain: 0 clear, 1 vanilla's colours).
|
||||
+ sky_extra: [f32; 4],
|
||||
+ /// Vanilla's sunrise colour fan (rgb, alpha), taken out of the sky before the atmosphere is put in.
|
||||
+ sunrise: [f32; 4],
|
||||
}
|
||||
|
||||
/// What the world around looks like this frame (from the renderer's globals).
|
||||
@@ -131,6 +138,9 @@ pub struct HdScene {
|
||||
pub camera_mod: [f32; 2],
|
||||
/// The water effect may run (not underwater, not disabled).
|
||||
pub water: bool,
|
||||
+ /// Vanilla's sunrise fan colour and its sky fog end (to subtract vanilla's sky from the world).
|
||||
+ pub sunrise: [f32; 4],
|
||||
+ pub sky_fog_end: f32,
|
||||
}
|
||||
|
||||
/// The cascades' half sizes (blocks) and shadow map size.
|
||||
@@ -155,6 +165,9 @@ struct HdFrame {
|
||||
view_proj: mat4x4<f32>,
|
||||
water: vec4<f32>,
|
||||
sky_color: vec4<f32>,
|
||||
+ sky_sun: vec4<f32>,
|
||||
+ sky_extra: vec4<f32>,
|
||||
+ sunrise: vec4<f32>,
|
||||
}
|
||||
struct FullOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@@ -195,6 +208,9 @@ struct Light {
|
||||
@group(0) @binding(13) var ao_tex: texture_2d<f32>;
|
||||
// The water pass's depth: the nearest water surface (0 where there is none).
|
||||
@group(0) @binding(14) var water_depth: texture_depth_2d;
|
||||
+//SKY15
|
||||
+// The pixel being lit, for the shadow filter's noise.
|
||||
+var<private> frag_px: vec2<f32>;
|
||||
|
||||
// The occlusion at a full-resolution pixel `dist` away: the four nearest half-resolution texels
|
||||
// weighted by closeness and by how near their distance is, so it doesn't bleed across edges.
|
||||
@@ -239,6 +255,42 @@ fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
||||
return shadow_taps(p, n, 1);
|
||||
}
|
||||
|
||||
+// A point of a disc of radius 1 (Vogel's spiral), turned by `angle`.
|
||||
+fn vogel(i: i32, count: i32, angle: f32) -> vec2<f32> {
|
||||
+ let r = sqrt((f32(i) + 0.5) / f32(count));
|
||||
+ let a = f32(i) * 2.39996323 + angle;
|
||||
+ return vec2<f32>(cos(a), sin(a)) * r;
|
||||
+}
|
||||
+
|
||||
+// The near cascade's soft shadow: a blocker search sets the penumbra's width (wider the farther
|
||||
+// the shadow's caster is above the receiver), then a ring of taps turned by a per-pixel noise
|
||||
+// filters it; the temporal pass averages the noise. `s`: the point in the cascade (uv, depth),
|
||||
+// `texel`: the cascade's texel in blocks.
|
||||
+fn soft_shadow(s: vec3<f32>, texel: f32) -> f32 {
|
||||
+ let angle = ign(frag_px) * 6.2831853;
|
||||
+ var blocker = 0.0;
|
||||
+ var found = 0.0;
|
||||
+ for (var i = 0; i < 8; i++) {
|
||||
+ let o = vogel(i, 8, angle) * (6.0 / 2048.0);
|
||||
+ let q = clamp(vec2<i32>((s.xy + o) * 2048.0), vec2<i32>(0), vec2<i32>(2047));
|
||||
+ let z = textureLoad(shadow_map, q, 0, 0);
|
||||
+ if z < s.z - 0.0006 {
|
||||
+ blocker += z;
|
||||
+ found += 1.0;
|
||||
+ }
|
||||
+ }
|
||||
+ if found < 0.5 { return 1.0; }
|
||||
+ // The caster's height above the receiver, in blocks (the cascade's depth spans 799 blocks).
|
||||
+ let height = (s.z - blocker / found) * 799.0;
|
||||
+ let radius = clamp(height * 0.18 / texel, 1.6, 8.0);
|
||||
+ var lit = 0.0;
|
||||
+ for (var i = 0; i < 12; i++) {
|
||||
+ let o = vogel(i, 12, angle + 1.7) * (radius / 2048.0);
|
||||
+ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + o, 0, s.z - 0.0005 - 0.00005 * radius);
|
||||
+ }
|
||||
+ return lit / 12.0;
|
||||
+}
|
||||
+
|
||||
// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and
|
||||
// blurs it anyway).
|
||||
fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
|
||||
@@ -249,13 +301,16 @@ fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
|
||||
let s = cascade_at(q, c);
|
||||
if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
|
||||
var lit = 0.0;
|
||||
+ let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
||||
+ if r == 1 && c == 0 {
|
||||
+ return mix(1.0, soft_shadow(s, texel), fade);
|
||||
+ }
|
||||
let step = 1.0 / 2048.0;
|
||||
for (var y = -r; y <= r; y++) {
|
||||
for (var x = -r; x <= r; x++) {
|
||||
lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
|
||||
}
|
||||
}
|
||||
- let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
||||
return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
|
||||
}
|
||||
|
||||
@@ -300,7 +355,31 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let halo_start = tiles[tile + 2u];
|
||||
let halo_all = tiles[tile + 3u] == 0xffffffffu;
|
||||
let halo_count = select(tiles[tile + 3u], count, halo_all);
|
||||
+ frag_px = in.position.xy;
|
||||
+ let atmosphere = frame.sky_sun.w > 0.5;
|
||||
var c = scene;
|
||||
+ // The sky: the atmosphere's, with what the world drew over vanilla's plain sky (sun, moon,
|
||||
+ // stars, clouds) kept on top.
|
||||
+ if d <= 0.0 && atmosphere {
|
||||
+ var kept = max(scene - vanilla_sky(view), vec3<f32>(0.0));
|
||||
+ var sky = atmosphere_sky(view);
|
||||
+ // The sun: vanilla's square sprite is taken out and a round disc with a soft haze round
|
||||
+ // it (warm, strongest low) is drawn in its place.
|
||||
+ let toward = dot(view, frame.sky_sun.xyz);
|
||||
+ kept *= 1.0 - smoothstep(0.965, 0.975, toward);
|
||||
+ let clear = 1.0 - frame.sky_extra.z;
|
||||
+ let shine = smoothstep(-0.04, 0.02, frame.sky_sun.y) * clear;
|
||||
+ let glow = max(toward, 0.0);
|
||||
+ let angle = acos(clamp(toward, -1.0, 1.0));
|
||||
+ sky += frame.sun_color.rgb * shine * (pow(glow, 24.0) * 0.07 + pow(glow, 400.0) * 0.4 + 6.0 * (1.0 - smoothstep(0.016, 0.022, angle)));
|
||||
+ c = sky + kept;
|
||||
+ }
|
||||
+ // Clouds (vanilla's, drawn into the world with depth but not into the G-buffer): lit by the low
|
||||
+ // sun's colour at dusk and dawn instead of vanilla's flat white.
|
||||
+ if atmosphere && !water && d > 0.0 && g <= 0.0 && p.y > 8.0 && dot(p, p) > 2304.0 {
|
||||
+ let low = 1.0 - smoothstep(0.05, 0.5, frame.sky_sun.y);
|
||||
+ c = scene * mix(vec3<f32>(1.0), mix(frame.sun_color.rgb, vec3<f32>(1.0), 0.45) * 1.1, low * 0.7 * (1.0 - frame.sky_extra.z));
|
||||
+ }
|
||||
let surface = water || (d > 0.0 && abs(d - g) <= max(d, g) * 0.002);
|
||||
if surface {
|
||||
let n = normalize(textureLoad(normal_tex, px, 0).xyz);
|
||||
@@ -323,7 +402,17 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
||||
if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); }
|
||||
let ao_ambient = mix(1.0, occlusion, frame.ambient.w);
|
||||
let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5);
|
||||
- var light = max(base * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
|
||||
+ // The sky's light is bluish from above and carries the horizon's colour from the sides;
|
||||
+ // faces turned up take more of it than the ones turned down.
|
||||
+ var sky_tint = vec3<f32>(1.0);
|
||||
+ if atmosphere {
|
||||
+ let reach = smoothstep(0.25, 0.9, lm.x);
|
||||
+ let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
||||
+ let shape = mix(0.66, 1.1, smoothstep(0.1, 1.0, up));
|
||||
+ let hue = mix(vec3<f32>(1.0), sky_light_tint(n), 0.4 * (1.0 - 0.5 * frame.sun_color.a));
|
||||
+ sky_tint = mix(vec3<f32>(1.0), hue * shape, reach);
|
||||
+ }
|
||||
+ var light = max(base * sky_tint * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
|
||||
// The mods' lights.
|
||||
for (var k = 0u; k < surface_count; k++) {
|
||||
let l = lights[select(tiles[surface_start + k], k, surface_all)];
|
||||
@@ -340,7 +429,12 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let sph = length(p);
|
||||
let cyl = max(length(p.xz), abs(p.y));
|
||||
let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
|
||||
- c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a);
|
||||
+ let haze = haze_colour(view);
|
||||
+ c = mix(c, haze, f * frame.fog_color.a);
|
||||
+ // Aerial perspective: far things take the horizon's colour a little before the fog does.
|
||||
+ if atmosphere && frame.fog.x > 1.0e6 {
|
||||
+ c = mix(c, haze, (1.0 - exp(-sph / 700.0)) * 0.5);
|
||||
+ }
|
||||
}
|
||||
// Light in the air: the sun's rays between the shadows, and halos round the mods' lights.
|
||||
let far = select(min(length(p), frame.shadow.z), frame.shadow.z, d <= 0.0);
|
||||
@@ -387,6 +481,7 @@ const REFLECT_RANGE: f32 = 96.0;
|
||||
@group(0) @binding(7) var shadow_sampler: sampler_comparison;
|
||||
@group(0) @binding(8) var lightmap: texture_2d<f32>;
|
||||
@group(0) @binding(9) var linear: sampler;
|
||||
+//SKY10
|
||||
|
||||
fn ign(px: vec2<f32>) -> f32 {
|
||||
let p = px + vec2<f32>(5.588238 * frame.params.w);
|
||||
@@ -417,7 +512,7 @@ fn texel(uv: vec2<f32>) -> vec2<i32> {
|
||||
// are whole cycles per 600 s, so the sum repeats every 256 blocks and 10 minutes and holds still
|
||||
// in the world as the camera moves. x, y: the vector in units of 2 pi / 256; z: the slope the
|
||||
// wave adds; w: its phase.
|
||||
-const WAVES = array<vec4<f32>, 10>(
|
||||
+const WAVES = array<vec4<f32>, 26>(
|
||||
vec4<f32>(3.0, 1.0, 0.040, 0.3),
|
||||
vec4<f32>(-2.0, 5.0, 0.045, 2.1),
|
||||
vec4<f32>(7.0, 2.0, 0.040, 4.4),
|
||||
@@ -428,17 +523,36 @@ const WAVES = array<vec4<f32>, 10>(
|
||||
vec4<f32>(9.0, 23.0, 0.030, 2.7),
|
||||
vec4<f32>(-27.0, 19.0, 0.025, 4.9),
|
||||
vec4<f32>(31.0, 30.0, 0.025, 1.8),
|
||||
+ // Fine ripples (wavelengths of 0.7 to 3 blocks) for the many small sun sparkles: slopes small,
|
||||
+ // faded out by distance and pixel size like the rest.
|
||||
+ vec4<f32>(83.0, 41.0, 0.042, 0.4),
|
||||
+ vec4<f32>(-97.0, 59.0, 0.042, 3.1),
|
||||
+ vec4<f32>(71.0, -113.0, 0.042, 5.0),
|
||||
+ vec4<f32>(131.0, 89.0, 0.040, 1.5),
|
||||
+ vec4<f32>(-157.0, -103.0, 0.040, 4.2),
|
||||
+ vec4<f32>(191.0, -67.0, 0.038, 2.4),
|
||||
+ vec4<f32>(-223.0, 149.0, 0.038, 0.8),
|
||||
+ vec4<f32>(101.0, 241.0, 0.036, 3.7),
|
||||
+ vec4<f32>(-263.0, -181.0, 0.034, 5.5),
|
||||
+ vec4<f32>(311.0, 127.0, 0.034, 2.0),
|
||||
+ // Finer still (0.35 to 0.6 blocks): the small sparkles.
|
||||
+ vec4<f32>(397.0, -263.0, 0.050, 1.1),
|
||||
+ vec4<f32>(-467.0, 331.0, 0.050, 4.6),
|
||||
+ vec4<f32>(521.0, 449.0, 0.045, 3.0),
|
||||
+ vec4<f32>(-613.0, -199.0, 0.045, 5.9),
|
||||
+ vec4<f32>(293.0, 577.0, 0.045, 0.2),
|
||||
+ vec4<f32>(-701.0, 263.0, 0.040, 2.8),
|
||||
);
|
||||
|
||||
// The gradient of the water's height at world x, z: the waves that a pixel `footprint` blocks
|
||||
// wide can still resolve (finer ones fade out, or they would shimmer).
|
||||
fn wave_gradient(xz: vec2<f32>, footprint: f32) -> vec2<f32> {
|
||||
var grad = vec2<f32>(0.0);
|
||||
- for (var i = 0; i < 10; i++) {
|
||||
+ for (var i = 0; i < 26; i++) {
|
||||
let w = WAVES[i];
|
||||
let k = w.xy * (2.0 * PI / 256.0);
|
||||
let len = length(k);
|
||||
- let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.25 - 0.5, 0.0, 1.0);
|
||||
+ let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.2 - 0.6, 0.0, 1.0);
|
||||
let cycles = round(sqrt(9.8 * len) * 0.5 * 600.0 / (2.0 * PI));
|
||||
let omega = cycles * 2.0 * PI / 600.0;
|
||||
let phase = dot(k, xz) - omega * frame.water.y + w.w;
|
||||
@@ -481,7 +595,9 @@ fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
|
||||
// the zenith's, darker where the water lies under cover (`vis` is the sky light there).
|
||||
fn sky_along(r: vec3<f32>, vis: f32) -> vec3<f32> {
|
||||
var c = frame.fog_color.rgb;
|
||||
- if frame.sky_color.w > 0.5 {
|
||||
+ if frame.sky_sun.w > 0.5 {
|
||||
+ c = atmosphere_sky(r);
|
||||
+ } else if frame.sky_color.w > 0.5 {
|
||||
c = mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, r.y));
|
||||
}
|
||||
return c * mix(0.12, 1.0, vis);
|
||||
@@ -531,7 +647,7 @@ fn reflection(p: vec3<f32>, r: vec3<f32>, jitter: f32) -> vec4<f32> {
|
||||
// The sun's glint on the waves (GGX, a rough lobe so it glitters instead of aliasing).
|
||||
fn glint(n: vec3<f32>, v: vec3<f32>, l: vec3<f32>) -> f32 {
|
||||
let h = normalize(l + v);
|
||||
- let a = 0.05;
|
||||
+ let a = 0.07;
|
||||
let nh = max(dot(n, h), 0.0);
|
||||
let d = a * a / (PI * pow(nh * nh * (a * a - 1.0) + 1.0, 2.0));
|
||||
let f = 0.02 + 0.98 * pow(1.0 - max(dot(v, h), 0.0), 5.0);
|
||||
@@ -565,7 +681,10 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
||||
// Light on the water: vanilla's lightmap at the surface, and the sun where it reaches.
|
||||
let levels = vec2<f32>(nt.a, tint.a);
|
||||
let sky_reach = smoothstep(0.35, 0.95, levels.x);
|
||||
- let ambient = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
|
||||
+ var ambient = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
|
||||
+ if frame.sky_sun.w > 0.5 {
|
||||
+ ambient *= mix(vec3<f32>(1.0), sky_light_tint(vec3<f32>(0.0, 1.0, 0.0)), 0.7 * sky_reach);
|
||||
+ }
|
||||
let block = textureSampleLevel(lightmap, linear, vec2<f32>(levels.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
|
||||
let sun = frame.sun.xyz;
|
||||
let sun_on = frame.sun.w;
|
||||
@@ -594,10 +713,10 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
||||
// A deep, dark water: the biome's colour darkened and a little greyed, so it reads as
|
||||
// depth rather than as bright paint.
|
||||
let biome = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
|
||||
- let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.8) * vec3<f32>(0.55, 0.68, 0.8);
|
||||
+ let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.85) * vec3<f32>(0.92, 1.0, 1.0);
|
||||
// The colours the water takes up fastest are the ones it has the least of.
|
||||
- let absorb = exp(-(vec3<f32>(1.0) - colour) * 1.1 * (thickness + 0.3));
|
||||
- let refracted = behind * absorb + colour * volume_light * 0.6 * (vec3<f32>(1.0) - absorb);
|
||||
+ let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.8 * (thickness + 0.3));
|
||||
+ let refracted = behind * absorb + colour * volume_light * mix(0.85, 0.4, frame.sun_color.a) * (vec3<f32>(1.0) - absorb);
|
||||
|
||||
// Reflection: the screen where the ray stays on it, the sky where it leaves it or misses.
|
||||
var r = reflect(-v, n);
|
||||
@@ -617,7 +736,11 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let sph = dist;
|
||||
let cyl = max(length(p.xz), abs(p.y));
|
||||
let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
|
||||
- c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a);
|
||||
+ let haze = haze_colour(-v);
|
||||
+ c = mix(c, haze, f * frame.fog_color.a);
|
||||
+ if frame.sky_sun.w > 0.5 && frame.fog.x > 1.0e6 {
|
||||
+ c = mix(c, haze, (1.0 - exp(-dist / 700.0)) * 0.5);
|
||||
+ }
|
||||
return vec4<f32>(max(c, vec3<f32>(0.0)), 1.0);
|
||||
}
|
||||
"#;
|
||||
@@ -1200,6 +1323,14 @@ pub struct Hd {
|
||||
/// The frame's translucent terrain exists (set by the renderer before `write`).
|
||||
pub water_geometry: bool,
|
||||
water_composited: bool,
|
||||
+ /// The sky-view texture (see `hd_sky`), the pass that marches it and its bind group.
|
||||
+ sky_lut: wgpu::TextureView,
|
||||
+ sky_pipeline: wgpu::RenderPipeline,
|
||||
+ sky_group: wgpu::BindGroup,
|
||||
+ /// The sun's height the sky-view texture was marched for (it depends on nothing else), and
|
||||
+ /// whether this frame's sun has moved enough to march it again.
|
||||
+ sky_drawn_for: f32,
|
||||
+ sky_stale: bool,
|
||||
started: std::time::Instant,
|
||||
/// TAA: the resolved frame, and the two histories (one read, one written, alternating).
|
||||
taa: wgpu::TextureView,
|
||||
@@ -1403,10 +1534,23 @@ fn ao_strength_default() -> f32 {
|
||||
.unwrap_or(AO_STRENGTH)
|
||||
}
|
||||
|
||||
+/// The shader's source: the common part, then the body with the sky-reading functions where it
|
||||
+/// has a marker (`//SKY15`: the sky-view texture is binding 15, `//SKY10`: binding 10).
|
||||
+fn assemble(body: &str) -> String {
|
||||
+ let mut body = body.to_owned();
|
||||
+ for binding in ["15", "10"] {
|
||||
+ body = body.replace(
|
||||
+ &format!("//SKY{binding}"),
|
||||
+ &hd_sky::SKY_FUNCS.replace("SKYBIND", binding),
|
||||
+ );
|
||||
+ }
|
||||
+ format!("{COMMON}{body}")
|
||||
+}
|
||||
+
|
||||
fn module(device: &wgpu::Device, label: &str, body: &str) -> wgpu::ShaderModule {
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(label),
|
||||
- source: wgpu::ShaderSource::Wgsl(format!("{COMMON}{body}").into()),
|
||||
+ source: wgpu::ShaderSource::Wgsl(assemble(body).into()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1523,6 +1667,7 @@ impl Hd {
|
||||
},
|
||||
UNFILTERED,
|
||||
DEPTH,
|
||||
+ FLOAT,
|
||||
]),
|
||||
});
|
||||
let water_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
@@ -1542,8 +1687,13 @@ impl Hd {
|
||||
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
|
||||
FLOAT,
|
||||
SAMPLER,
|
||||
+ FLOAT,
|
||||
]),
|
||||
});
|
||||
+ let sky_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
+ label: Some("HD sky"),
|
||||
+ entries: &entries(&[UNIFORM]),
|
||||
+ });
|
||||
let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("HD TAA"),
|
||||
entries: &entries(&[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER]),
|
||||
@@ -1605,6 +1755,16 @@ impl Hd {
|
||||
let ao = module(device, "HD ambient occlusion", AO);
|
||||
let ao_blur = module(device, "HD ambient occlusion blur", AO_BLUR);
|
||||
let water = module(device, "HD water", WATER);
|
||||
+ let sky_module = module(device, "HD sky", hd_sky::SKY_PASS);
|
||||
+ let sky_pipeline = fullscreen(
|
||||
+ device,
|
||||
+ "HD sky",
|
||||
+ &sky_layout,
|
||||
+ &sky_module,
|
||||
+ "fs_sky_lut",
|
||||
+ hd_sky::SKY_LUT_FORMAT,
|
||||
+ None,
|
||||
+ );
|
||||
let water_pipeline = fullscreen(
|
||||
device,
|
||||
"HD water",
|
||||
@@ -1691,6 +1851,21 @@ impl Hd {
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
+ let sky_lut = target(
|
||||
+ device,
|
||||
+ "HD sky",
|
||||
+ hd_sky::SKY_LUT_SIZE,
|
||||
+ hd_sky::SKY_LUT_FORMAT,
|
||||
+ )
|
||||
+ .view;
|
||||
+ let sky_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
+ label: Some("HD sky"),
|
||||
+ layout: &sky_layout,
|
||||
+ entries: &[wgpu::BindGroupEntry {
|
||||
+ binding: 0,
|
||||
+ resource: frame.as_entire_binding(),
|
||||
+ }],
|
||||
+ });
|
||||
let lights = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("HD lights"),
|
||||
size: (MAX_LIGHTS * std::mem::size_of::<HdLight>()) as u64,
|
||||
@@ -1788,6 +1963,11 @@ impl Hd {
|
||||
water_active: false,
|
||||
water_geometry: false,
|
||||
water_composited: false,
|
||||
+ sky_lut,
|
||||
+ sky_pipeline,
|
||||
+ sky_group,
|
||||
+ sky_drawn_for: 2.0,
|
||||
+ sky_stale: true,
|
||||
started: std::time::Instant::now(),
|
||||
taa: target(device, "HD TAA", [1, 1], HDR).view,
|
||||
history: [
|
||||
@@ -1961,6 +2141,11 @@ impl Hd {
|
||||
if !self.taa_active || scene.camera_delta.length() > 64.0 {
|
||||
self.history_ready = false;
|
||||
}
|
||||
+ let sun_height = scene.sun.normalize_or(glam::Vec3::Y).y;
|
||||
+ self.sky_stale = (sun_height - self.sky_drawn_for).abs() > 0.001;
|
||||
+ if self.sky_stale {
|
||||
+ self.sky_drawn_for = sun_height;
|
||||
+ }
|
||||
let frame = HdFrame {
|
||||
prev_view_proj: scene.prev_view_proj.to_cols_array_2d(),
|
||||
taa: [
|
||||
@@ -1982,6 +2167,17 @@ impl Hd {
|
||||
scene.sky_color[2],
|
||||
if scene.sky { 1.0 } else { 0.0 },
|
||||
],
|
||||
+ sky_sun: {
|
||||
+ let sun = scene.sun.normalize_or(glam::Vec3::Y);
|
||||
+ [sun.x, sun.y, sun.z, if scene.sky { 1.0 } else { 0.0 }]
|
||||
+ },
|
||||
+ sky_extra: [
|
||||
+ scene.sky_fog_end,
|
||||
+ 0.0,
|
||||
+ (scene.rain / 0.7).clamp(0.0, 1.0) * 0.85,
|
||||
+ 0.0,
|
||||
+ ],
|
||||
+ sunrise: scene.sunrise,
|
||||
taa_params: [
|
||||
if self.upscaling() {
|
||||
UPSCALE_HISTORY
|
||||
@@ -2123,6 +2319,23 @@ impl Hd {
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
+ /// Marches the sky-view texture for this frame's sun (before the lighting and the water); skipped
|
||||
+ /// while the sun's height has moved less than a thousandth since the last march.
|
||||
+ pub fn sky(&mut self, encoder: &mut wgpu::CommandEncoder) {
|
||||
+ if !self.sky_stale {
|
||||
+ return;
|
||||
+ }
|
||||
+ Self::draw(
|
||||
+ encoder,
|
||||
+ "HD sky",
|
||||
+ &self.sky_lut,
|
||||
+ false,
|
||||
+ &self.sky_pipeline,
|
||||
+ &self.sky_group,
|
||||
+ stamp(&mut self.timing, "hd sky"),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
/// Lights the world (its colour and depth) into `lit`.
|
||||
pub fn light(
|
||||
&mut self,
|
||||
@@ -2203,6 +2416,10 @@ impl Hd {
|
||||
binding: 14,
|
||||
resource: wgpu::BindingResource::TextureView(&self.water_depth),
|
||||
},
|
||||
+ wgpu::BindGroupEntry {
|
||||
+ binding: 15,
|
||||
+ resource: wgpu::BindingResource::TextureView(&self.sky_lut),
|
||||
+ },
|
||||
],
|
||||
});
|
||||
self.light_group = Some((key, group));
|
||||
@@ -2512,6 +2729,7 @@ impl Hd {
|
||||
wgpu::BindingResource::Sampler(&self.shadow_sampler),
|
||||
wgpu::BindingResource::TextureView(lightmap.0),
|
||||
wgpu::BindingResource::Sampler(&self.sampler),
|
||||
+ wgpu::BindingResource::TextureView(&self.sky_lut),
|
||||
],
|
||||
);
|
||||
self.water_group = Some((key, group));
|
||||
@@ -2608,9 +2826,13 @@ pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
|
||||
let clear = 1.0 - scene.rain * 0.85;
|
||||
if sun.y > -0.02 {
|
||||
let up = smooth(-0.02, 0.18, sun.y);
|
||||
- let warm = smooth(0.0, 0.45, sun.y);
|
||||
- let color = glam::Vec3::new(1.0, 0.62, 0.38).lerp(glam::Vec3::new(1.0, 0.95, 0.86), warm);
|
||||
- (sun, 0.6 * up * clear, color)
|
||||
+ // The atmosphere's transmittance along the sun's ray (orange when low, a clean warm white
|
||||
+ // high), kept at full brightness and tempered, with a slight warm tint on top.
|
||||
+ let t = hd_sky::sun_transmittance(sun.y);
|
||||
+ let hue = t / t.max_element().max(1e-4);
|
||||
+ let color = glam::Vec3::new(hue.x.powf(0.6), hue.y.powf(0.6), hue.z.powf(0.6))
|
||||
+ * glam::Vec3::new(1.0, 0.93, 0.78);
|
||||
+ (sun, 0.7 * up * clear, color)
|
||||
} else {
|
||||
let moon = -sun;
|
||||
let up = smooth(-0.02, 0.18, moon.y);
|
||||
@@ -2661,8 +2883,9 @@ mod tests {
|
||||
("ao", AO),
|
||||
("ao blur", AO_BLUR),
|
||||
("water", WATER),
|
||||
+ ("sky", hd_sky::SKY_PASS),
|
||||
] {
|
||||
- let source = format!("{COMMON}{body}");
|
||||
+ let source = assemble(body);
|
||||
let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| {
|
||||
panic!("{name}: {}", e.emit_to_string(&source));
|
||||
});
|
||||
diff --git a/crates/horizon-client/src/gfx/hd_sky.rs b/crates/horizon-client/src/gfx/hd_sky.rs
|
||||
new file mode 100644
|
||||
index 000000000..b5c231a7b
|
||||
--- /dev/null
|
||||
+++ b/crates/horizon-client/src/gfx/hd_sky.rs
|
||||
@@ -0,0 +1,324 @@
|
||||
+//! AirDef fork: the HD renderer's atmosphere. A small sky-view texture (azimuth from the sun by
|
||||
+//! elevation, plus a row of ambient samples) is marched each frame from single scattering of
|
||||
+//! Rayleigh and Mie particles and ozone absorption with a cheap multiple-scattering term; the
|
||||
+//! lighting and water shaders read it for the sky, the aerial-perspective haze, the water's
|
||||
+//! reflection fallback and the colour of the sky's light. The sunlight's colour is the same
|
||||
+//! atmosphere's transmittance at the sun's elevation (`sun_transmittance`).
|
||||
+//!
|
||||
+//! The coefficients, the density profiles (scale heights, the ozone layer), the Henyey-Greenstein
|
||||
+//! Mie parameters and the Chapman-function transmittance come from Photon by sixthsurge (MIT
|
||||
+//! licence, see THIRD_PARTY_PHOTON.md), shaders/include/sky/atmosphere.glsl at commit 26c2906.
|
||||
+
|
||||
+/// The sky-view texture: 128 azimuths by 64 elevations, then 8 rows for the ambient samples
|
||||
+/// (row 64: texel 0 the zenith, texel 1 the horizon's average).
|
||||
+pub const SKY_LUT_SIZE: [u32; 2] = [128, 72];
|
||||
+pub const SKY_LUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
+
|
||||
+/// The atmosphere's constants and the pass that marches the sky-view texture (after `COMMON`).
|
||||
+pub const SKY_PASS: &str = r#"
|
||||
+@group(0) @binding(0) var<uniform> frame: HdFrame;
|
||||
+
|
||||
+// Photon's atmosphere (sixthsurge, MIT): metres, Earth's size; the camera stands on the surface.
|
||||
+const PI: f32 = 3.14159265;
|
||||
+const PLANET_R: f32 = 6371000.0;
|
||||
+const ATMOSPHERE_TOP: f32 = 6481000.0;
|
||||
+const GROUND_R: f32 = 6370000.0;
|
||||
+const SCALE_HEIGHTS: vec2<f32> = vec2<f32>(8400.0, 1250.0);
|
||||
+const RAYLEIGH: vec3<f32> = vec3<f32>(8.059375432e-06, 1.671209429e-05, 4.080133294e-05);
|
||||
+const MIE: vec3<f32> = vec3<f32>(1.666442358e-06, 1.812685127e-06, 1.958927896e-06);
|
||||
+const OZONE: vec3<f32> = vec3<f32>(8.304280072e-07, 1.314911970e-06, 5.440679729e-08);
|
||||
+const MIE_ALBEDO: f32 = 0.9;
|
||||
+const MIE_G: f32 = 0.77;
|
||||
+// Sunlight in space (Photon's AM0 colour) and how bright the sun and the moon shine here.
|
||||
+const SUN_I: vec3<f32> = vec3<f32>(1.051, 0.985, 0.940) * 24.0;
|
||||
+const MOON_I: vec3<f32> = vec3<f32>(0.62, 0.78, 1.0) * 0.07;
|
||||
+// The share of the light scattered a second time, spread evenly (the haze's lift).
|
||||
+const MULTI_SCATTER: f32 = 0.6;
|
||||
+const STEPS: i32 = 32;
|
||||
+
|
||||
+// Photon's density: Rayleigh, Mie (exponential) and the ozone layer, at an altitude in metres.
|
||||
+fn density_at(altitude: f32) -> vec3<f32> {
|
||||
+ let rm = exp(-max(altitude, 0.0) / SCALE_HEIGHTS);
|
||||
+ let km = altitude * 1e-3;
|
||||
+ let o1 = 12.5 * exp(-km / 8.0);
|
||||
+ let o2 = 30.0 * exp((18.0 - km) * (km - 18.0) / 80.0);
|
||||
+ let o3 = 75.0 * exp((23.5 - km) * (km - 23.5) / 50.0);
|
||||
+ let o4 = 50.0 * exp((30.0 - km) * (km - 30.0) / 150.0);
|
||||
+ return vec3<f32>(rm, 7.428e-3 * (o1 + o2 + o3 + o4));
|
||||
+}
|
||||
+
|
||||
+// Chapman function approximation (thetenthplanet.de/archives/4519, as Photon uses it).
|
||||
+fn chapman(x: f32, cos_theta: f32) -> f32 {
|
||||
+ let c = sqrt(1.5707963 * x);
|
||||
+ if cos_theta >= 0.0 {
|
||||
+ return c / ((c - 1.0) * cos_theta + 1.0);
|
||||
+ }
|
||||
+ let sin_theta = sqrt(clamp(1.0 - cos_theta * cos_theta, 0.0, 1.0));
|
||||
+ return c / ((c - 1.0) * cos_theta - 1.0) + 2.0 * c * exp(min(x - x * sin_theta, 80.0)) * sqrt(sin_theta);
|
||||
+}
|
||||
+
|
||||
+// The light reaching a point r from the planet's centre (altitude h, whose sky direction makes
|
||||
+// cos mu with the up there) from the sun or the moon; 0 when the planet is in the way.
|
||||
+fn light_transmittance(mu: f32, r: f32, h: f32) -> vec3<f32> {
|
||||
+ if mu < 0.0 && r * r * (1.0 - mu * mu) < PLANET_R * PLANET_R { return vec3<f32>(0.0); }
|
||||
+ let dens = exp(-max(h, 0.0) / SCALE_HEIGHTS);
|
||||
+ var air = SCALE_HEIGHTS * dens;
|
||||
+ air.x *= chapman(r / SCALE_HEIGHTS.x, mu);
|
||||
+ air.y *= chapman(r / SCALE_HEIGHTS.y, mu);
|
||||
+ return clamp(exp(-(RAYLEIGH * air.x + MIE * air.y + OZONE * air.x)), vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
+}
|
||||
+
|
||||
+fn rayleigh_phase(nu: f32) -> f32 {
|
||||
+ return 3.0 / (16.0 * PI) * (1.0 + nu * nu);
|
||||
+}
|
||||
+
|
||||
+fn mie_phase(nu: f32) -> f32 {
|
||||
+ let g = MIE_G;
|
||||
+ return (1.0 - g * g) / (4.0 * PI * pow(1.0 + g * g - 2.0 * g * nu, 1.5));
|
||||
+}
|
||||
+
|
||||
+// The radiance the camera sees along `view` (y up) with the sun and the moon in the directions
|
||||
+// given: single scattering plus a flat multiple-scattering term.
|
||||
+fn march(view: vec3<f32>, sun: vec3<f32>, moon: vec3<f32>) -> vec3<f32> {
|
||||
+ let b = PLANET_R * view.y;
|
||||
+ let t_max = -b + sqrt(b * b + ATMOSPHERE_TOP * ATMOSPHERE_TOP - PLANET_R * PLANET_R);
|
||||
+ let nu_sun = dot(view, sun);
|
||||
+ let nu_moon = dot(view, moon);
|
||||
+ let phase_r_sun = rayleigh_phase(nu_sun);
|
||||
+ let phase_m_sun = mie_phase(nu_sun);
|
||||
+ let phase_r_moon = rayleigh_phase(nu_moon);
|
||||
+ let phase_m_moon = mie_phase(nu_moon);
|
||||
+ var od = vec3<f32>(0.0);
|
||||
+ var sum = vec3<f32>(0.0);
|
||||
+ var t0 = 0.0;
|
||||
+ for (var i = 0; i < STEPS; i++) {
|
||||
+ let u = (f32(i) + 1.0) / f32(STEPS);
|
||||
+ let t1 = t_max * u * u;
|
||||
+ let dt = t1 - t0;
|
||||
+ let t = 0.5 * (t0 + t1);
|
||||
+ t0 = t1;
|
||||
+ let r2 = PLANET_R * PLANET_R + 2.0 * PLANET_R * t * view.y + t * t;
|
||||
+ let r = sqrt(r2);
|
||||
+ // The altitude without the cancellation of r - planet radius.
|
||||
+ let altitude = (2.0 * PLANET_R * t * view.y + t * t) / (r + PLANET_R);
|
||||
+ let dens = density_at(altitude);
|
||||
+ let extinction = RAYLEIGH * dens.x + MIE * dens.y + OZONE * dens.z;
|
||||
+ let view_t = exp(-(od + 0.5 * extinction * dt));
|
||||
+ od += extinction * dt;
|
||||
+ let mu_sun = (PLANET_R * sun.y + t * nu_sun) / r;
|
||||
+ let mu_moon = (PLANET_R * moon.y + t * nu_moon) / r;
|
||||
+ let sun_t = light_transmittance(mu_sun, r, altitude);
|
||||
+ let moon_t = light_transmittance(mu_moon, r, altitude);
|
||||
+ let scatter_r = RAYLEIGH * dens.x;
|
||||
+ let scatter_m = MIE * MIE_ALBEDO * dens.y;
|
||||
+ let single = (scatter_r * phase_r_sun + scatter_m * phase_m_sun) * sun_t * SUN_I
|
||||
+ + (scatter_r * phase_r_moon + scatter_m * phase_m_moon) * moon_t * MOON_I;
|
||||
+ let multi = (scatter_r + scatter_m) * (1.0 / (4.0 * PI)) * MULTI_SCATTER
|
||||
+ * (sun_t * SUN_I + moon_t * MOON_I) * 0.5;
|
||||
+ sum += view_t * (single + multi) * dt;
|
||||
+ }
|
||||
+ return sum;
|
||||
+}
|
||||
+
|
||||
+@fragment
|
||||
+fn fs_sky_lut(in: FullOut) -> @location(0) vec4<f32> {
|
||||
+ let px = vec2<i32>(in.position.xy);
|
||||
+ // The texture's frame: the sun in the xy plane, the moon opposite.
|
||||
+ let sin_e = clamp(frame.sky_sun.y, -1.0, 1.0);
|
||||
+ let sun = vec3<f32>(sqrt(1.0 - sin_e * sin_e), sin_e, 0.0);
|
||||
+ let moon = -sun;
|
||||
+ // A faint night glow, so the night sky is not black.
|
||||
+ let night = vec3<f32>(0.0006, 0.0011, 0.0026);
|
||||
+ if px.y < 64 {
|
||||
+ let phi = f32(px.x) / 127.0 * PI;
|
||||
+ let v = f32(px.y) / 63.0;
|
||||
+ let l = v * v * PI * 0.5;
|
||||
+ let dir = vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi));
|
||||
+ return vec4<f32>(march(dir, sun, moon) + night, 1.0);
|
||||
+ }
|
||||
+ if px.y == 64 && px.x == 0 {
|
||||
+ return vec4<f32>(march(vec3<f32>(0.0, 1.0, 0.0), sun, moon) + night, 1.0);
|
||||
+ }
|
||||
+ if px.y == 64 && px.x == 1 {
|
||||
+ var sum = vec3<f32>(0.0);
|
||||
+ for (var k = 0; k < 6; k++) {
|
||||
+ let phi = (f32(k) + 0.5) / 6.0 * PI;
|
||||
+ let l = 0.3;
|
||||
+ sum += march(vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi)), sun, moon);
|
||||
+ }
|
||||
+ return vec4<f32>(sum / 6.0 + night, 1.0);
|
||||
+ }
|
||||
+ return vec4<f32>(0.0);
|
||||
+}
|
||||
+"#;
|
||||
+
|
||||
+/// Reading the sky-view texture, in the lighting and the water shaders (`SKYBIND` is replaced by
|
||||
+/// the binding of the texture; `frame` and the `linear` sampler come before it).
|
||||
+pub const SKY_FUNCS: &str = r#"
|
||||
+@group(0) @binding(SKYBIND) var sky_lut: texture_2d<f32>;
|
||||
+const SKY_PI: f32 = 3.14159265;
|
||||
+// How bright the sky is shown (the exposure of the physical radiance) and its gamma.
|
||||
+const SKY_EXPOSURE: f32 = 0.85;
|
||||
+
|
||||
+// Where the direction `dir` (world, y up) lies in the sky-view texture.
|
||||
+fn sky_uv(dir: vec3<f32>) -> vec2<f32> {
|
||||
+ let s = frame.sky_sun.xyz;
|
||||
+ let l = asin(clamp(dir.y, 0.0, 1.0));
|
||||
+ var cphi = 1.0;
|
||||
+ let dl = length(dir.xz);
|
||||
+ let sl = length(s.xz);
|
||||
+ if dl > 1e-4 && sl > 1e-4 { cphi = dot(dir.xz / dl, s.xz / sl); }
|
||||
+ let u = acos(clamp(cphi, -1.0, 1.0)) / SKY_PI;
|
||||
+ let v = sqrt(l / (SKY_PI * 0.5));
|
||||
+ return vec2<f32>((u * 127.0 + 0.5) / 128.0, (v * 63.0 + 0.5) / 72.0);
|
||||
+}
|
||||
+
|
||||
+// The physical sky's colour as it is shown: a soft shoulder, then the display's gamma.
|
||||
+fn sky_show(radiance: vec3<f32>) -> vec3<f32> {
|
||||
+ // Compressed by the brightest channel, so the hue stays as the atmosphere made it.
|
||||
+ let peak = max(max(radiance.r, radiance.g), max(radiance.b, 1e-6));
|
||||
+ let c = pow(radiance * ((1.0 - exp(-peak * SKY_EXPOSURE)) / peak), vec3<f32>(1.0 / 1.9));
|
||||
+ // A little more colour (Photon boosts the atmosphere's saturation too).
|
||||
+ // The low sun gets more, for orange and pink in place of pastel.
|
||||
+ let boost = 1.2 + 0.45 * (1.0 - smoothstep(0.0, 0.35, frame.sky_sun.y));
|
||||
+ return max(mix(vec3<f32>(dot(c, vec3<f32>(0.2126, 0.7152, 0.0722))), c, boost), vec3<f32>(0.0));
|
||||
+}
|
||||
+
|
||||
+// The vanilla sky's own gradient (the overcast and the rain keep it).
|
||||
+fn vanilla_gradient(dir: vec3<f32>) -> vec3<f32> {
|
||||
+ return mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, max(dir.y, 0.0)));
|
||||
+}
|
||||
+
|
||||
+// The sky along `dir`: the atmosphere, greyed towards vanilla's colours in rain.
|
||||
+fn atmosphere_sky(dir: vec3<f32>) -> vec3<f32> {
|
||||
+ let lin = textureSampleLevel(sky_lut, linear, sky_uv(dir), 0.0).rgb;
|
||||
+ var c = sky_show(lin);
|
||||
+ let overcast = frame.sky_extra.z;
|
||||
+ if overcast > 0.0 { c = mix(c, vanilla_gradient(dir), overcast); }
|
||||
+ return c;
|
||||
+}
|
||||
+
|
||||
+// The colour distant things fade to: the sky just above the horizon in their direction (so the
|
||||
+// haze leans towards the sun), or vanilla's fog colour where the environment sets its own fog.
|
||||
+fn haze_colour(dir: vec3<f32>) -> vec3<f32> {
|
||||
+ if frame.sky_sun.w < 0.5 || frame.fog.x < 1.0e6 { return frame.fog_color.rgb; }
|
||||
+ let up = max(dir.y, 0.0) * 0.5 + 0.035;
|
||||
+ return atmosphere_sky(normalize(vec3<f32>(dir.x, 0.0, dir.z) + vec3<f32>(0.0, up, 0.0)));
|
||||
+}
|
||||
+
|
||||
+// The sky's light for a surface of normal n: the horizon's colour from the sides, the zenith's
|
||||
+// from above, as a hue (luminance 1) to tint vanilla's sky light with.
|
||||
+fn sky_light_tint(n: vec3<f32>) -> vec3<f32> {
|
||||
+ let zenith = sky_show(textureLoad(sky_lut, vec2<i32>(0, 64), 0).rgb);
|
||||
+ let horizon = sky_show(textureLoad(sky_lut, vec2<i32>(1, 64), 0).rgb);
|
||||
+ let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
||||
+ let c = mix(horizon, zenith, smoothstep(0.35, 1.0, up));
|
||||
+ let luma = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
|
||||
+ return c / max(luma, 0.003);
|
||||
+}
|
||||
+
|
||||
+// vanilla's sky (terrain.wgsl: fs_sky), to tell what the world drew on the sky pixel (the sun and
|
||||
+// moon sprites, the stars, the clouds) from its plain gradient.
|
||||
+fn vanilla_disc_distance(d: vec3<f32>, y: f32) -> vec2<f32> {
|
||||
+ if d.y * y <= 0.0 { return vec2<f32>(-1.0); }
|
||||
+ let p = d * (y / d.y);
|
||||
+ let h = length(p.xz);
|
||||
+ let phi = atan2(p.z, p.x);
|
||||
+ let sector = SKY_PI / 4.0;
|
||||
+ let delta = phi - (floor(phi / sector) * sector + sector * 0.5);
|
||||
+ let edge = 512.0 * cos(sector * 0.5) / cos(delta);
|
||||
+ if h > edge { return vec2<f32>(-1.0); }
|
||||
+ let s = h / edge;
|
||||
+ let ay = abs(y);
|
||||
+ let rim = sqrt(512.0 * 512.0 + y * y);
|
||||
+ return vec2<f32>(ay + s * (rim - ay), ay + s * (512.0 - ay));
|
||||
+}
|
||||
+fn vanilla_linear_fog(d: f32, start: f32, end: f32) -> f32 {
|
||||
+ if d <= start { return 0.0; }
|
||||
+ if d >= end { return 1.0; }
|
||||
+ return (d - start) / (end - start);
|
||||
+}
|
||||
+fn vanilla_rotate_x90(v: vec3<f32>) -> vec3<f32> { return vec3<f32>(v.x, -v.z, v.y); }
|
||||
+fn vanilla_sunrise_weight(d: vec3<f32>) -> f32 {
|
||||
+ let alpha = frame.sunrise.a;
|
||||
+ if alpha <= 0.001 { return 0.0; }
|
||||
+ let flip = select(0.0, 180.0, frame.sky_sun.x > 0.0);
|
||||
+ let theta = radians(flip + 90.0);
|
||||
+ let c = cos(theta);
|
||||
+ let s = sin(theta);
|
||||
+ var best = 0.0;
|
||||
+ let apex = vanilla_rotate_x90(vec3<f32>(-100.0 * s, 100.0 * c, 0.0));
|
||||
+ for (var i = 0; i < 16; i++) {
|
||||
+ let a0 = f32(i) * 2.0 * SKY_PI / 16.0;
|
||||
+ let a1 = f32(i + 1) * 2.0 * SKY_PI / 16.0;
|
||||
+ let l0 = vec3<f32>(sin(a0) * 120.0, cos(a0) * 120.0, -cos(a0) * 40.0 * alpha);
|
||||
+ let l1 = vec3<f32>(sin(a1) * 120.0, cos(a1) * 120.0, -cos(a1) * 40.0 * alpha);
|
||||
+ let r0 = vanilla_rotate_x90(vec3<f32>(l0.x * c - l0.y * s, l0.x * s + l0.y * c, l0.z));
|
||||
+ let r1 = vanilla_rotate_x90(vec3<f32>(l1.x * c - l1.y * s, l1.x * s + l1.y * c, l1.z));
|
||||
+ let e1 = r0 - apex;
|
||||
+ let e2 = r1 - apex;
|
||||
+ let pv = cross(d, e2);
|
||||
+ let det = dot(e1, pv);
|
||||
+ if abs(det) < 1e-6 { continue; }
|
||||
+ let tv = -apex;
|
||||
+ let u = dot(tv, pv) / det;
|
||||
+ let qv = cross(tv, e1);
|
||||
+ let v = dot(d, qv) / det;
|
||||
+ let t = dot(e2, qv) / det;
|
||||
+ if u >= 0.0 && v >= 0.0 && u + v <= 1.0 && t > 0.0 {
|
||||
+ best = max(best, 1.0 - u - v);
|
||||
+ }
|
||||
+ }
|
||||
+ return best;
|
||||
+}
|
||||
+fn vanilla_sky(d: vec3<f32>) -> vec3<f32> {
|
||||
+ var color = frame.fog_color.rgb;
|
||||
+ let dist = vanilla_disc_distance(d, 16.0);
|
||||
+ if dist.x >= 0.0 {
|
||||
+ let end = frame.sky_extra.x;
|
||||
+ let f = max(vanilla_linear_fog(dist.x, 0.0, end), vanilla_linear_fog(dist.y, end, end));
|
||||
+ color = mix(frame.sky_color.rgb, frame.fog_color.rgb, f * frame.fog_color.a);
|
||||
+ }
|
||||
+ let w = vanilla_sunrise_weight(d);
|
||||
+ if w > 0.0 {
|
||||
+ color = mix(color, frame.sunrise.rgb, w * frame.sunrise.a);
|
||||
+ }
|
||||
+ return color;
|
||||
+}
|
||||
+"#;
|
||||
+
|
||||
+/// The sun's light at the ground: the atmosphere's transmittance along the sun's direction
|
||||
+/// (`sin_elevation`, the y of the unit vector towards it), linear, as a fraction of the light
|
||||
+/// outside the air. Same Chapman model as the sky pass.
|
||||
+pub fn sun_transmittance(sin_elevation: f32) -> glam::Vec3 {
|
||||
+ const PLANET: f32 = 6_371_000.0;
|
||||
+ const SCALE: [f32; 2] = [8400.0, 1250.0];
|
||||
+ const RAYLEIGH: glam::Vec3 = glam::Vec3::new(8.059_375e-6, 1.671_209_4e-5, 4.080_133_4e-5);
|
||||
+ const MIE: glam::Vec3 = glam::Vec3::new(1.666_442_4e-6, 1.812_685e-6, 1.958_927_9e-6);
|
||||
+ const OZONE: glam::Vec3 = glam::Vec3::new(8.304_28e-7, 1.314_912e-6, 5.440_679_7e-8);
|
||||
+ // A sun below the horizon keeps the horizon's colour (its strength is faded elsewhere).
|
||||
+ let mu = sin_elevation.clamp(0.0, 1.0);
|
||||
+ let chapman = |x: f32, c: f32| {
|
||||
+ let k = (std::f32::consts::FRAC_PI_2 * x).sqrt();
|
||||
+ k / ((k - 1.0) * c + 1.0)
|
||||
+ };
|
||||
+ let air = [
|
||||
+ SCALE[0] * chapman(PLANET / SCALE[0], mu),
|
||||
+ SCALE[1] * chapman(PLANET / SCALE[1], mu),
|
||||
+ ];
|
||||
+ let tau = RAYLEIGH * air[0] + MIE * air[1] + OZONE * air[0];
|
||||
+ glam::Vec3::new((-tau.x).exp(), (-tau.y).exp(), (-tau.z).exp())
|
||||
+}
|
||||
+
|
||||
+#[cfg(test)]
|
||||
+mod tests {
|
||||
+ use super::*;
|
||||
+
|
||||
+ #[test]
|
||||
+ fn the_low_sun_is_redder_than_the_high_one() {
|
||||
+ let noon = sun_transmittance(1.0);
|
||||
+ let low = sun_transmittance(0.1);
|
||||
+ assert!(noon.x > 0.85 && noon.z > 0.6, "{noon:?}");
|
||||
+ assert!(low.z / low.x < noon.z / noon.x * 0.6, "{low:?} vs {noon:?}");
|
||||
+ }
|
||||
+}
|
||||
diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl
|
||||
index e892d5a0f..0a2a6cdba 100644
|
||||
--- a/crates/horizon-client/src/terrain.wgsl
|
||||
+++ b/crates/horizon-client/src/terrain.wgsl
|
||||
@@ -197,7 +197,7 @@ struct WaterOut {
|
||||
if layer != WATER_STILL && layer != WATER_FLOW { discard; }
|
||||
// The biome's colour at the texture's mean brightness, not the texture: the HD water draws
|
||||
// its own surface (waves, reflections), and vanilla's ripples on top would look painted on.
|
||||
- let color = vec3(0.62) * in.tint.rgb;
|
||||
+ let color = vec3(0.85) * in.tint.rgb;
|
||||
var out: WaterOut;
|
||||
out.tint = vec4(color, clamp(in.lm.y, 0.0, 1.0));
|
||||
out.normal = vec4(face_normal(in.pos) * 0.5 + 0.5, clamp(in.lm.x, 0.0, 1.0));
|
||||
--
|
||||
2.54.0 (Apple Git-157)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
From 6277e051a89c2537570982f6ef275887193c05cf Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 19:02:16 +0300
|
||||
Subject: [PATCH 22/22] HD water: a darker blue, a little clearer, ripples a
|
||||
little higher
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
crates/horizon-client/src/gfx/hd.rs | 9 +++++----
|
||||
1 file changed, 5 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
||||
index 031e9f2de..f078d423c 100644
|
||||
--- a/crates/horizon-client/src/gfx/hd.rs
|
||||
+++ b/crates/horizon-client/src/gfx/hd.rs
|
||||
@@ -558,7 +558,8 @@ fn wave_gradient(xz: vec2<f32>, footprint: f32) -> vec2<f32> {
|
||||
let phase = dot(k, xz) - omega * frame.water.y + w.w;
|
||||
grad += (k / len) * (w.z * fade * cos(phase));
|
||||
}
|
||||
- return grad;
|
||||
+ // A little higher than the table's slopes.
|
||||
+ return grad * 1.25;
|
||||
}
|
||||
|
||||
fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
|
||||
@@ -713,10 +714,10 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
||||
// A deep, dark water: the biome's colour darkened and a little greyed, so it reads as
|
||||
// depth rather than as bright paint.
|
||||
let biome = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
|
||||
- let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.85) * vec3<f32>(0.92, 1.0, 1.0);
|
||||
+ let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.85) * vec3<f32>(0.78, 0.86, 0.95);
|
||||
// The colours the water takes up fastest are the ones it has the least of.
|
||||
- let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.8 * (thickness + 0.3));
|
||||
- let refracted = behind * absorb + colour * volume_light * mix(0.85, 0.4, frame.sun_color.a) * (vec3<f32>(1.0) - absorb);
|
||||
+ let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.6 * (thickness + 0.3));
|
||||
+ let refracted = behind * absorb + colour * volume_light * mix(0.65, 0.3, frame.sun_color.a) * (vec3<f32>(1.0) - absorb);
|
||||
|
||||
// Reflection: the screen where the ray stays on it, the sky where it leaves it or misses.
|
||||
var r = reflect(-v, n);
|
||||
--
|
||||
2.54.0 (Apple Git-157)
|
||||
|
||||
Reference in New Issue
Block a user