init
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Copy to .env and fill in. Used by tools/pixellab.py (never committed).
|
||||||
|
PIXELLAB_API_KEY=
|
||||||
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
build/
|
||||||
|
build-*/
|
||||||
|
cmake-build-*/
|
||||||
|
out/
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# Runtime output
|
||||||
|
screenshots/
|
||||||
|
|
||||||
|
# Editors / tooling
|
||||||
|
.cache/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"blender": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["blender-mcp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
CMakeLists.txt
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(hd2d LANGUAGES C CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
# --- Dependencies -----------------------------------------------------------
|
||||||
|
# System packages are preferred; anything missing is fetched.
|
||||||
|
find_package(OpenGL REQUIRED)
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(EPOXY REQUIRED IMPORTED_TARGET epoxy)
|
||||||
|
|
||||||
|
find_package(glfw3 3.3 QUIET)
|
||||||
|
if(NOT glfw3_FOUND)
|
||||||
|
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_Declare(glfw GIT_REPOSITORY https://github.com/glfw/glfw.git GIT_TAG 3.4)
|
||||||
|
FetchContent_MakeAvailable(glfw)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(glm QUIET)
|
||||||
|
if(NOT glm_FOUND)
|
||||||
|
FetchContent_Declare(glm GIT_REPOSITORY https://github.com/g-truc/glm.git GIT_TAG 1.0.1)
|
||||||
|
FetchContent_MakeAvailable(glm)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_path(STB_INCLUDE_DIR stb_image.h PATH_SUFFIXES stb)
|
||||||
|
if(NOT STB_INCLUDE_DIR)
|
||||||
|
FetchContent_Declare(stb GIT_REPOSITORY https://github.com/nothings/stb.git GIT_TAG master)
|
||||||
|
FetchContent_MakeAvailable(stb)
|
||||||
|
set(STB_INCLUDE_DIR ${stb_SOURCE_DIR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_path(CGLTF_INCLUDE_DIR cgltf.h)
|
||||||
|
if(NOT CGLTF_INCLUDE_DIR)
|
||||||
|
FetchContent_Declare(cgltf GIT_REPOSITORY https://github.com/jkuhlmann/cgltf.git GIT_TAG v1.14)
|
||||||
|
FetchContent_MakeAvailable(cgltf)
|
||||||
|
set(CGLTF_INCLUDE_DIR ${cgltf_SOURCE_DIR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
FetchContent_Declare(imgui GIT_REPOSITORY https://github.com/ocornut/imgui.git GIT_TAG v1.91.9b)
|
||||||
|
FetchContent_MakeAvailable(imgui)
|
||||||
|
add_library(imgui STATIC
|
||||||
|
${imgui_SOURCE_DIR}/imgui.cpp
|
||||||
|
${imgui_SOURCE_DIR}/imgui_draw.cpp
|
||||||
|
${imgui_SOURCE_DIR}/imgui_tables.cpp
|
||||||
|
${imgui_SOURCE_DIR}/imgui_widgets.cpp
|
||||||
|
${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
|
||||||
|
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp)
|
||||||
|
target_include_directories(imgui PUBLIC ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends)
|
||||||
|
target_link_libraries(imgui PUBLIC glfw OpenGL::GL)
|
||||||
|
|
||||||
|
# --- Engine -----------------------------------------------------------------
|
||||||
|
file(GLOB_RECURSE HD2D_ENGINE_SOURCES CONFIGURE_DEPENDS engine/src/*.cpp)
|
||||||
|
add_library(hd2d STATIC ${HD2D_ENGINE_SOURCES})
|
||||||
|
target_include_directories(hd2d PUBLIC engine/include)
|
||||||
|
target_include_directories(hd2d SYSTEM PRIVATE ${STB_INCLUDE_DIR} ${CGLTF_INCLUDE_DIR}) # silence third-party warnings
|
||||||
|
target_link_libraries(hd2d PUBLIC glfw glm::glm PkgConfig::EPOXY imgui OpenGL::GL)
|
||||||
|
target_compile_definitions(hd2d PUBLIC GLM_FORCE_RADIANS GLM_ENABLE_EXPERIMENTAL)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(hd2d PRIVATE /W4)
|
||||||
|
else()
|
||||||
|
target_compile_options(hd2d PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- Demo -------------------------------------------------------------------
|
||||||
|
add_executable(hd2d_demo demo/main.cpp demo/DemoScene.cpp)
|
||||||
|
target_link_libraries(hd2d_demo PRIVATE hd2d)
|
||||||
|
# Assets/shaders are loaded relative to this directory at runtime.
|
||||||
|
target_compile_definitions(hd2d_demo PRIVATE HD2D_ASSET_DIR="${CMAKE_SOURCE_DIR}/assets")
|
||||||
227
README.md
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# HD-2D Engine
|
||||||
|
|
||||||
|
A small C++20 / OpenGL 4.5 engine that recreates the look of Square Enix's
|
||||||
|
**HD-2D** style (Octopath Traveler, Triangle Strategy, Live A Live, Dragon
|
||||||
|
Quest III HD-2D): a 3D diorama world where the ground is skinned in pixel art,
|
||||||
|
characters and small props (flowers, grass tufts, tall grass) are billboarded
|
||||||
|
pixel-art sprites, and scenery like trees, bushes and rocks are real **3D
|
||||||
|
models with higher-resolution painted textures**. Lighting, shadows, ambient
|
||||||
|
occlusion, fog, god rays, water, fire, rain and particles are all rendered as
|
||||||
|
high-resolution 3D effects.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## What makes HD-2D look like HD-2D
|
||||||
|
|
||||||
|
| Trait | How this engine does it |
|
||||||
|
| --- | --- |
|
||||||
|
| 3D world built from pixel-art textures | Tile-based terraced terrain (`Terrain`) with pixel-art material layers, organically jittered material borders at texel granularity, grass overhangs on cliffs, baked corner AO |
|
||||||
|
| Crisp pixels at any distance | "Sharp bilinear" sampling (`pixelart.glsl`): hard texel edges when magnified, antialiased + mipmapped when minified (no `GL_NEAREST` shimmer) |
|
||||||
|
| Upright 2D sprites in 3D | Characters and small props are camera-facing (Y-axis) billboards (`BillboardBatch`), planted by their lowest opaque pixel row |
|
||||||
|
| 3D scenery with painted textures | Trees, bushes, rocks and the stone bridge are instanced glTF models (`ModelLibrary`, `ModelBatch`) built in Blender: leaf-card canopies with alpha-tested 1K leaf textures, rounded custom normals for soft stylized shading, sun translucency, wind sway from per-vertex weights, emissive materials (glowing coals) |
|
||||||
|
| Grounded objects | Two kinds of ambient occlusion: half-res SSAO (depth-reconstructed normals, depth-aware blur, multiplied into the opaque scene before water) and a baked **ground contact occlusion** map (`GroundOcclusion`) that puts a dark core at every trunk / bush / rock base plus a soft canopy shade, applied to terrain, sprites and models near the ground |
|
||||||
|
| Nothing hides the player | Scenery between the camera and the player is dithered away around the sprite |
|
||||||
|
| Sprites cast/receive real shadows | Shadow pass renders sprites as **sun-facing** cards so silhouettes are never edge-on; receivers sample their own sun-facing card to avoid self-shadowing |
|
||||||
|
| Miniature / diorama feel | Narrow FOV (~30°) high camera + strong depth-of-field with an extra tilt-shift band |
|
||||||
|
| Soft, warm, glowing light | HDR pipeline, Karis-averaged mip-chain bloom, ACES tonemap, lift/gain grading, vignette, subtle chromatic aberration and grain |
|
||||||
|
| Fog and god rays | Half-res ray-marched height fog; every step samples the sun shadow map and a sparse "canopy" beam pattern, giving physically shadowed light shafts; depth-aware upsample |
|
||||||
|
| Dappled forest light | Animated canopy cookie projected along the sun direction onto every surface |
|
||||||
|
| Beautiful water | Planar reflections, depth-aware refraction, per-channel absorption, analytic-derivative wave normals, shore distance field driving foam + ripples, strong HDR sun glints plus a field of twinkling point sparkles that bloom and depth of field turn into bokeh, caustics on the bottom |
|
||||||
|
| Living foliage | Instanced swaying grass with wind gusts; tall grass and bushes bend and rustle away from the player (vertex shader, Pokémon-style patches); leaf flutter scales with wind strength |
|
||||||
|
| High-res particles | Fireflies (also feed point lights) and dust motes that only glow inside sun shafts |
|
||||||
|
| Fire | Brazier fires (`ParticleSystem::addFire`): noise-eroded additive flame tongues with a hot-core-to-red color ramp, turbulent embers, lit smoke puffs, a glow halo, and a flickering point light that also scatters into the fog |
|
||||||
|
| Weather | `Weather` blends the clear look into a storm preset (weak grey sun, overcast sky, thick low fog, gusty wind): procedural world-space rain streaks (up to 18k, one draw call), ground splashes, raindrop ripple rings on the water, wet darker glossy surfaces, ambient fireflies fade out |
|
||||||
|
|
||||||
|
References: Unreal Engine's [Octopath Traveler spotlight](https://www.unrealengine.com/spotlights/octopath-traveler-s-hd-2d-art-style-and-story-make-for-a-jrpg-dream-come-true),
|
||||||
|
the [Octopath Traveler II interview](https://www.unrealengine.com/developer-interviews/octopath-traveler-ii-builds-a-bigger-bolder-world-in-its-stunning-hd-2d-style),
|
||||||
|
the Unreal Fest Europe 2019 talk ["The Fusion of Nostalgia and Novelty"](https://www.unrealengine.com/events/unreal-fest-europe-2019/the-fusion-of-nostalgia-and-novelty-in-the-development-of-octopath-traveler),
|
||||||
|
and [The Verge on HD-2D's evolution](https://www.theverge.com/entertainment/840080/square-enix-hd2d-games-octopath-dragon-quest).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requirements: CMake ≥ 3.20, a C++20 compiler, OpenGL 4.5, `libepoxy`.
|
||||||
|
GLFW, GLM, stb and cgltf are used from the system when found and fetched
|
||||||
|
otherwise; Dear ImGui is always fetched.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Arch / CachyOS
|
||||||
|
sudo pacman -S cmake ninja glfw glm libepoxy stb
|
||||||
|
|
||||||
|
cmake -S . -B build -G Ninja
|
||||||
|
ninja -C build
|
||||||
|
./build/hd2d_demo
|
||||||
|
```
|
||||||
|
|
||||||
|
The asset path is baked in at configure time (`HD2D_ASSET_DIR`), so the demo can
|
||||||
|
be run from any working directory.
|
||||||
|
|
||||||
|
## Demo controls
|
||||||
|
|
||||||
|
| Input | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| WASD / arrows | Move the sprite (camera-relative) |
|
||||||
|
| Q / E, right-drag | Rotate / orbit camera |
|
||||||
|
| Mouse wheel | Zoom |
|
||||||
|
| F1 | Toggle the tuning panels (sun, sky, AO, fog, god rays, water, DoF, bloom, grading, weather) |
|
||||||
|
| R | Toggle heavy rain (5 s transition) |
|
||||||
|
| F5 | Hot-reload shaders |
|
||||||
|
| F12 | Screenshot to `screenshots/` |
|
||||||
|
| Esc | Quit |
|
||||||
|
|
||||||
|
Scriptable captures (useful for look-dev and regression checks):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build/hd2d_demo --screenshot out.png --frames 60 --pos 25,43.6 \
|
||||||
|
--sun -35,38 --yaw 20 --dist 12 --move 1,0 --rain 1 --set godRays=6 --set dof=0
|
||||||
|
```
|
||||||
|
|
||||||
|
`--rain 0..1` starts the capture already raining. `--set` accepts:
|
||||||
|
`volumetrics dof bloom reflections occluderFade ssao groundAO ssaoRadius
|
||||||
|
ssaoIntensity fogDensity fogHeightFalloff godRays shaftStrength shaftCoverage
|
||||||
|
shaftScale cookieStrength exposure bloomStrength tiltShift focusRange`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
engine/include/hd2d/ public headers
|
||||||
|
engine/src/ engine implementation
|
||||||
|
assets/shaders/ GLSL (common.glsl = shared UBO + lighting helpers)
|
||||||
|
assets/textures/ PixelLab-generated pixel art (terrain tiles, grass, flowers)
|
||||||
|
assets/sprites/ character sprites
|
||||||
|
assets/models/ scenery.glb (trees, bushes, rocks, bridge) + src/ painted textures
|
||||||
|
demo/ meadow demo scene + main loop
|
||||||
|
tools/pixellab.py pixel-art generator, driven by tools/assets.json
|
||||||
|
tools/scenery_textures.py paints the high-res scenery textures
|
||||||
|
tools/blender_scenery.py builds + exports the 3D scenery in Blender
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frame pipeline (`Renderer::render`)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[Sun shadow map<br/>terrain, 3D models, sun-facing sprite cards] --> B[Planar reflection<br/>half res, clipped]
|
||||||
|
B --> C[HDR scene<br/>sky, terrain, models, billboards]
|
||||||
|
C --> C2[SSAO<br/>half res, multiplied in]
|
||||||
|
C2 --> D[Water<br/>reads color/depth copies]
|
||||||
|
D --> E[Volumetric fog + god rays<br/>half res ray march, fire haze]
|
||||||
|
E --> F[Fog composite<br/>depth-aware upsample]
|
||||||
|
F --> F2[Rain streaks]
|
||||||
|
F2 --> G[Particles<br/>fire, smoke, fireflies]
|
||||||
|
G --> H[Depth of field<br/>tilt-shift]
|
||||||
|
H --> I[Bloom mip chain]
|
||||||
|
I --> J[Tonemap + grade]
|
||||||
|
```
|
||||||
|
|
||||||
|
All per-frame data lives in one std140 uniform block (`FrameUBO` in
|
||||||
|
`Renderer.cpp` ⇄ `Frame` in `common.glsl`); keep them in sync.
|
||||||
|
|
||||||
|
### Using the engine in your own project
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
hd2d::Window window; window.create(1600, 900, "My Game");
|
||||||
|
hd2d::Renderer renderer; renderer.init("assets/shaders", window.width(), window.height());
|
||||||
|
|
||||||
|
hd2d::Terrain terrain; /* init, setLevel/setMaterial, textures.load(...), build() */
|
||||||
|
hd2d::BillboardBatch hero; hero.loadTexture("assets/sprites/hero.png");
|
||||||
|
hero.instances.push_back({{10, 0, 10}, 0.0f, hero.sizeForDensity(32.0f)});
|
||||||
|
hero.upload();
|
||||||
|
|
||||||
|
hd2d::ModelLibrary models; models.load("assets/models/scenery.glb");
|
||||||
|
hd2d::ModelBatch oaks(models.get("oak_a"));
|
||||||
|
oaks.instances.push_back({{14, 0, 12}, /*yaw*/ 0.7f, /*scale*/ glm::vec3(1.0f), /*tint*/ 1.0f});
|
||||||
|
oaks.upload();
|
||||||
|
|
||||||
|
hd2d::Scene scene;
|
||||||
|
scene.terrain = &terrain;
|
||||||
|
scene.billboards = {&hero};
|
||||||
|
scene.models = {&oaks};
|
||||||
|
|
||||||
|
hd2d::GroundOcclusion contactAO; // dark contact shadow at tree bases
|
||||||
|
oaks.groundOcclusion = 1.0f;
|
||||||
|
contactAO.build({0, 0}, {64, 64}, {&oaks});
|
||||||
|
scene.groundOcclusion = &contactAO;
|
||||||
|
|
||||||
|
hd2d::ParticleSystem particles; // fire: flames, embers, smoke, glow, light
|
||||||
|
particles.addFire({12, 1.5f, 12});
|
||||||
|
scene.particles = &particles;
|
||||||
|
scene.volumetricLights = 1; // the fire light also glows in the fog
|
||||||
|
|
||||||
|
hd2d::Weather weather; // weather.setRain(1.0f) for a storm
|
||||||
|
hd2d::Camera cam; cam.target = {10, 0.7f, 10};
|
||||||
|
|
||||||
|
float time = 0.0f;
|
||||||
|
while (window.pollEvents()) {
|
||||||
|
time += 1.0f / 60.0f;
|
||||||
|
particles.update(1.0f / 60.0f, time);
|
||||||
|
scene.lights.clear();
|
||||||
|
particles.collectLights(scene.lights, 12);
|
||||||
|
weather.update(1.0f / 60.0f, scene, renderer.settings);
|
||||||
|
renderer.render(scene, cam, time);
|
||||||
|
window.swapBuffers();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`demo/DemoScene.cpp` is the full reference: terrain generation, prop
|
||||||
|
scattering, bridge walking, fires from model sockets, contact AO, water +
|
||||||
|
shore field, particles, weather, player movement and collision.
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
### 3D scenery (Blender)
|
||||||
|
|
||||||
|
Trees, bushes, rocks and the stone bridge (with braziers) are generated
|
||||||
|
procedurally so they can be tweaked and rebuilt at any time:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/scenery_textures.py # paints assets/models/src/*.png (numpy + Pillow)
|
||||||
|
blender -b -P tools/blender_scenery.py # builds the models, writes assets/models/scenery.glb
|
||||||
|
```
|
||||||
|
|
||||||
|
`blender_scenery.py` can also be run inside a live Blender session (e.g. through
|
||||||
|
the Blender MCP) with `exec(open(".../tools/blender_scenery.py").read())`; it
|
||||||
|
only touches its own `HD2D_Scenery` collection and lays the models out side by
|
||||||
|
side for inspection. Any glTF works with `ModelLibrary`; the engine relies on
|
||||||
|
a few conventions:
|
||||||
|
|
||||||
|
- every root node with a mesh becomes a model, looked up by node name;
|
||||||
|
- `COLOR_0.rgb` multiplies the base color (tint / baked AO), `COLOR_0.a` is the
|
||||||
|
wind weight (0 rigid, 1 fully swaying);
|
||||||
|
- materials named `foliage*` are alpha-tested, two-sided and get wrap lighting,
|
||||||
|
translucency and soft self-shadowing; others honor glTF `alphaMode` and
|
||||||
|
`doubleSided`;
|
||||||
|
- foliage looks best with custom normals pointing away from the canopy center;
|
||||||
|
- emissive materials (glTF `emissiveFactor` / `emissiveTexture` /
|
||||||
|
`KHR_materials_emissive_strength`) glow and bloom;
|
||||||
|
- child empties of a model node become named **sockets** (`Model::sockets`,
|
||||||
|
model space; `ModelInstance::toWorld` places them). The bridge exports
|
||||||
|
`fire_0` / `fire_1` in its braziers and the demo spawns fires there.
|
||||||
|
|
||||||
|
### Pixel art (PixelLab)
|
||||||
|
|
||||||
|
`tools/pixellab.py` generates pixel-art textures through the
|
||||||
|
[PixelLab API](https://api.pixellab.ai/v2/llms.txt). Put your key in `.env`
|
||||||
|
(see `.env.example`); assets are declared in `tools/assets.json`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/pixellab.py --list # what exists
|
||||||
|
python3 tools/pixellab.py # generate missing assets
|
||||||
|
python3 tools/pixellab.py tall_grass # regenerate one asset
|
||||||
|
python3 tools/pixellab.py --balance
|
||||||
|
```
|
||||||
|
|
||||||
|
- `"kind": "image"` uses `/create-image-pixflux` (transparent sprites).
|
||||||
|
- `"kind": "tiles"` uses `/create-tiles-pro` (seamless top-down tiles); every
|
||||||
|
variation is saved as `<name>_<n>.png`. A `"tile_id"` re-downloads an existing
|
||||||
|
set without paying again.
|
||||||
|
|
||||||
|
Pixel-art texel density is 32 px per world unit; the 256 px test sprite is 4×
|
||||||
|
upscaled 64 px art, so it is rendered at 128 px per unit.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
The demo covers the rendering core. Natural next steps for a full engine:
|
||||||
|
sprite-sheet animation + 4/8-direction facing, a scene/level file format and
|
||||||
|
editor, more weather (snow, thunderstorms with lightning, wind-blown leaves),
|
||||||
|
time-of-day presets, cascaded shadows for large maps, audio, and an
|
||||||
|
entity/component layer for gameplay.
|
||||||
BIN
assets/models/scenery.glb
Normal file
BIN
assets/models/src/bark.png
Normal file
|
After Width: | Height: | Size: 535 KiB |
BIN
assets/models/src/coals.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
assets/models/src/cobble.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/models/src/iron.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
assets/models/src/leaves_bush.png
Normal file
|
After Width: | Height: | Size: 442 KiB |
BIN
assets/models/src/leaves_dense.png
Normal file
|
After Width: | Height: | Size: 800 KiB |
BIN
assets/models/src/leaves_oak.png
Normal file
|
After Width: | Height: | Size: 420 KiB |
BIN
assets/models/src/pine_frond.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
assets/models/src/rock.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/models/src/stone_blocks.png
Normal file
|
After Width: | Height: | Size: 915 KiB |
52
assets/shaders/billboard.frag
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "pixelart.glsl"
|
||||||
|
|
||||||
|
in VS {
|
||||||
|
vec2 uv;
|
||||||
|
vec3 worldPos;
|
||||||
|
vec3 pivot;
|
||||||
|
vec2 local;
|
||||||
|
float tint;
|
||||||
|
} v;
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D uTex;
|
||||||
|
uniform int uPass;
|
||||||
|
uniform float uEmissive;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 c = sharpSample(uTex, v.uv);
|
||||||
|
if (c.a < 0.5) discard;
|
||||||
|
if (uPass == 1) return;
|
||||||
|
|
||||||
|
vec3 up = vec3(0.0, 1.0, 0.0);
|
||||||
|
vec3 toCam = cross(uBillboardRight.xyz, up);
|
||||||
|
// Sprites are flat cards; a normal leaning upward keeps them readable
|
||||||
|
// under top lighting while still responding to light direction.
|
||||||
|
vec3 n = normalize(toCam * 0.7 + up * 0.7);
|
||||||
|
vec3 L = uSunDir.xyz;
|
||||||
|
float ndl = clamp((dot(n, L) + 0.55) / 1.55, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Receive shadows at the matching point on this sprite's own sun-facing
|
||||||
|
// shadow card, nudged toward the sun: no self-shadowing, correct
|
||||||
|
// occlusion by everything else.
|
||||||
|
vec3 sp = v.pivot + uShadowRight.xyz * v.local.x + vec3(0.0, v.local.y, 0.0) + L * 0.22;
|
||||||
|
float vis = sunShadow(sp) * canopyCookie(v.worldPos);
|
||||||
|
|
||||||
|
vec3 albedo = c.rgb * v.tint * mix(1.0, 0.8, uWeather.y); // rain-soaked sprites read darker
|
||||||
|
float gao = groundAO(v.pivot, v.local.y);
|
||||||
|
vec3 light = uSunColor.rgb * uSunDir.w * ndl * vis * mix(1.0, gao, 0.7) + hemiAmbient(n) * gao +
|
||||||
|
pointLights(v.worldPos, n);
|
||||||
|
// Thin-foliage translucency when the sun is behind the sprite.
|
||||||
|
float behind = max(dot(-toCam, L), 0.0);
|
||||||
|
light += uSunColor.rgb * uSunDir.w * behind * behind * vis * 0.25;
|
||||||
|
|
||||||
|
vec3 col = albedo * light + albedo * uEmissive;
|
||||||
|
if (uBlob.w > 0.0) { // fold the player's contact shadow onto grass sprites too
|
||||||
|
float d = length(v.pivot.xz - uBlob.xz) / uBlob.w;
|
||||||
|
col *= 1.0 - 0.35 * (1.0 - smoothstep(0.3, 1.0, d)) * (1.0 - clamp(v.local.y / 0.6, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
fragColor = vec4(col, 1.0);
|
||||||
|
}
|
||||||
58
assets/shaders/billboard.vert
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 iPosSway; // pivot xyz, sway
|
||||||
|
layout(location = 1) in vec4 iSizeFlipTint; // size xy, flip, tint
|
||||||
|
|
||||||
|
uniform vec4 uOpaque; // opaque pixel bounds in uv (minX, minY, maxX, maxY)
|
||||||
|
uniform int uPass; // 0 = color, 1 = shadow
|
||||||
|
|
||||||
|
out VS {
|
||||||
|
vec2 uv;
|
||||||
|
vec3 worldPos;
|
||||||
|
vec3 pivot;
|
||||||
|
vec2 local; // (horizontal offset, height above pivot) in world units
|
||||||
|
float tint;
|
||||||
|
} o;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1));
|
||||||
|
vec2 size = iSizeFlipTint.xy;
|
||||||
|
vec3 pivot = iPosSway.xyz;
|
||||||
|
float lx = (corner.x - 0.5) * size.x;
|
||||||
|
float ly = (corner.y - uOpaque.y) * size.y; // lowest opaque row sits on the pivot
|
||||||
|
|
||||||
|
// Shadow cards face the sun so silhouettes are never edge-on to the light.
|
||||||
|
vec3 right = uPass == 1 ? uShadowRight.xyz : uBillboardRight.xyz;
|
||||||
|
vec3 wp = pivot + right * lx + vec3(0.0, ly, 0.0);
|
||||||
|
|
||||||
|
float sway = iPosSway.w;
|
||||||
|
if (sway > 0.0) {
|
||||||
|
float visibleHeight = max(size.y * (uOpaque.w - uOpaque.y), 1e-3);
|
||||||
|
float k = clamp(ly / visibleHeight, 0.0, 1.0);
|
||||||
|
k *= k;
|
||||||
|
float ph = dot(pivot.xz, vec2(0.37, 0.61));
|
||||||
|
vec2 wdir = normalize(uWind.xy + 1e-5);
|
||||||
|
float gust = vnoise(pivot.xz * 0.15 - wdir * uTime * uWind.w);
|
||||||
|
float w = (sin(uTime * 2.1 + ph) * 0.5 + sin(uTime * 3.7 + ph * 1.7) * 0.25 + gust * 1.3) * uWind.z;
|
||||||
|
vec2 disp = wdir * w;
|
||||||
|
// Bend away from the interactor (player) and rustle while it is close.
|
||||||
|
vec3 toP = pivot - uInteractor.xyz;
|
||||||
|
float d = length(toP.xz);
|
||||||
|
float push = clamp(1.0 - d / uInteractor.w, 0.0, 1.0) * step(abs(toP.y), 1.0);
|
||||||
|
disp += (d > 1e-3 ? toP.xz / d : vec2(0.0)) * push * 0.5;
|
||||||
|
disp += vec2(sin(uTime * 19.0 + ph * 7.0), cos(uTime * 17.0 + ph * 5.0)) * push * 0.05;
|
||||||
|
wp.xz += disp * k * sway * visibleHeight;
|
||||||
|
wp.y -= push * k * sway * 0.3 * visibleHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 uv = corner;
|
||||||
|
if (iSizeFlipTint.z > 0.5) uv.x = 1.0 - uv.x;
|
||||||
|
o.uv = uv;
|
||||||
|
o.worldPos = wp;
|
||||||
|
o.pivot = pivot;
|
||||||
|
o.local = vec2(lx, ly);
|
||||||
|
o.tint = iSizeFlipTint.w;
|
||||||
|
gl_Position = uViewProj * vec4(wp, 1.0);
|
||||||
|
gl_ClipDistance[0] = dot(vec4(wp, 1.0), uClipPlane);
|
||||||
|
}
|
||||||
40
assets/shaders/bloom_down.frag
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#version 450 core
|
||||||
|
// 13-tap downsample (Jimenez, "Next Generation Post Processing in CoD:AW").
|
||||||
|
// The first pass uses a Karis average to suppress fireflies.
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uSrc;
|
||||||
|
uniform vec2 uSrcTexel;
|
||||||
|
uniform int uKaris;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
float luma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
|
||||||
|
vec3 karis(vec3 c) { return c / (1.0 + luma(c)); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 t = uSrcTexel;
|
||||||
|
vec3 a = texture(uSrc, vUV + t * vec2(-2, 2)).rgb;
|
||||||
|
vec3 b = texture(uSrc, vUV + t * vec2(0, 2)).rgb;
|
||||||
|
vec3 c = texture(uSrc, vUV + t * vec2(2, 2)).rgb;
|
||||||
|
vec3 d = texture(uSrc, vUV + t * vec2(-2, 0)).rgb;
|
||||||
|
vec3 e = texture(uSrc, vUV).rgb;
|
||||||
|
vec3 f = texture(uSrc, vUV + t * vec2(2, 0)).rgb;
|
||||||
|
vec3 g = texture(uSrc, vUV + t * vec2(-2, -2)).rgb;
|
||||||
|
vec3 h = texture(uSrc, vUV + t * vec2(0, -2)).rgb;
|
||||||
|
vec3 i = texture(uSrc, vUV + t * vec2(2, -2)).rgb;
|
||||||
|
vec3 j = texture(uSrc, vUV + t * vec2(-1, 1)).rgb;
|
||||||
|
vec3 k = texture(uSrc, vUV + t * vec2(1, 1)).rgb;
|
||||||
|
vec3 l = texture(uSrc, vUV + t * vec2(-1, -1)).rgb;
|
||||||
|
vec3 m = texture(uSrc, vUV + t * vec2(1, -1)).rgb;
|
||||||
|
vec3 r;
|
||||||
|
if (uKaris == 1) {
|
||||||
|
vec3 g0 = karis((a + b + d + e) * 0.25), g1 = karis((b + c + e + f) * 0.25);
|
||||||
|
vec3 g2 = karis((d + e + g + h) * 0.25), g3 = karis((e + f + h + i) * 0.25);
|
||||||
|
vec3 g4 = karis((j + k + l + m) * 0.25);
|
||||||
|
r = g4 * 0.5 + (g0 + g1 + g2 + g3) * 0.125;
|
||||||
|
r /= max(1.0 - luma(r), 1e-3); // undo tonemap-weighting on the average
|
||||||
|
} else {
|
||||||
|
r = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
|
||||||
|
}
|
||||||
|
fragColor = vec4(max(r, vec3(0.0)), 1.0);
|
||||||
|
}
|
||||||
18
assets/shaders/bloom_up.frag
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#version 450 core
|
||||||
|
// 3x3 tent upsample, additively blended into the next larger mip.
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uSrc;
|
||||||
|
uniform vec2 uSrcTexel;
|
||||||
|
uniform float uRadius;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 t = uSrcTexel * uRadius;
|
||||||
|
vec3 s = texture(uSrc, vUV).rgb * 4.0;
|
||||||
|
s += (texture(uSrc, vUV + vec2(-t.x, 0)).rgb + texture(uSrc, vUV + vec2(t.x, 0)).rgb +
|
||||||
|
texture(uSrc, vUV + vec2(0, -t.y)).rgb + texture(uSrc, vUV + vec2(0, t.y)).rgb) * 2.0;
|
||||||
|
s += texture(uSrc, vUV + vec2(-t.x, -t.y)).rgb + texture(uSrc, vUV + vec2(t.x, -t.y)).rgb +
|
||||||
|
texture(uSrc, vUV + vec2(-t.x, t.y)).rgb + texture(uSrc, vUV + vec2(t.x, t.y)).rgb;
|
||||||
|
fragColor = vec4(s / 16.0, 1.0);
|
||||||
|
}
|
||||||
200
assets/shaders/common.glsl
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// Shared frame data and helpers. Mirrors FrameUBO in Renderer.cpp (std140).
|
||||||
|
layout(std140, binding = 0) uniform Frame {
|
||||||
|
mat4 uView;
|
||||||
|
mat4 uProj;
|
||||||
|
mat4 uViewProj;
|
||||||
|
mat4 uInvViewProj;
|
||||||
|
mat4 uLightViewProj;
|
||||||
|
vec4 uCamPos; // xyz, w = time (seconds)
|
||||||
|
vec4 uCamParams; // near, far, focus distance, unused
|
||||||
|
vec4 uScreen; // target width, height, 1/width, 1/height
|
||||||
|
vec4 uSunDir; // xyz toward sun, w = intensity
|
||||||
|
vec4 uSunColor; // rgb
|
||||||
|
vec4 uSkyZenith; // rgb, w = ambient intensity
|
||||||
|
vec4 uSkyHorizon; // rgb, w = camera-side fill light intensity
|
||||||
|
vec4 uSkyGround; // rgb
|
||||||
|
vec4 uFogColor; // rgb, w = density
|
||||||
|
vec4 uFogParams; // height falloff, base height, noise amount, anisotropy g
|
||||||
|
vec4 uFogParams2; // god ray intensity, max distance, steps, unused
|
||||||
|
vec4 uWind; // dir.xy, strength, gust speed
|
||||||
|
vec4 uInteractor; // xyz, radius
|
||||||
|
vec4 uBlob; // xyz, radius
|
||||||
|
vec4 uClipPlane; // world-space plane for gl_ClipDistance[0]
|
||||||
|
vec4 uBillboardRight;// camera-facing billboard right vector
|
||||||
|
vec4 uShadowRight; // sun-facing billboard right vector (shadow cards)
|
||||||
|
vec4 uCookie; // strength, scale, speed, coverage
|
||||||
|
vec4 uShadowParams; // texel size, softness, unused, unused
|
||||||
|
vec4 uShaft; // god ray beam strength, coverage, pattern scale, unused
|
||||||
|
vec4 uWaterParams; // level, caustics intensity, has water, unused
|
||||||
|
vec4 uGroundAO; // world -> uv: minX, minZ, 1/sizeX, 1/sizeZ (z = 0: disabled)
|
||||||
|
vec4 uWeather; // rain intensity, surface wetness, rain fall speed, unused
|
||||||
|
ivec4 uLightCount;
|
||||||
|
vec4 uLightPos[16]; // xyz, w = radius
|
||||||
|
vec4 uLightColor[16];
|
||||||
|
};
|
||||||
|
|
||||||
|
#define uTime uCamPos.w
|
||||||
|
#define PI 3.14159265359
|
||||||
|
|
||||||
|
layout(binding = 8) uniform sampler2DShadow uShadowMap;
|
||||||
|
layout(binding = 9) uniform sampler2D uGroundAOTex;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- noise ----
|
||||||
|
float hash12(vec2 p) {
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||||
|
p3 += dot(p3, p3.yzx + 33.33);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
float hash13(vec3 p3) {
|
||||||
|
p3 = fract(p3 * 0.1031);
|
||||||
|
p3 += dot(p3, p3.zyx + 31.32);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
float vnoise(vec2 p) {
|
||||||
|
vec2 i = floor(p), f = fract(p);
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
return mix(mix(hash12(i), hash12(i + vec2(1, 0)), u.x),
|
||||||
|
mix(hash12(i + vec2(0, 1)), hash12(i + vec2(1, 1)), u.x), u.y);
|
||||||
|
}
|
||||||
|
float vnoise3(vec3 p) {
|
||||||
|
vec3 i = floor(p), f = fract(p);
|
||||||
|
vec3 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
float a = mix(mix(hash13(i), hash13(i + vec3(1, 0, 0)), u.x),
|
||||||
|
mix(hash13(i + vec3(0, 1, 0)), hash13(i + vec3(1, 1, 0)), u.x), u.y);
|
||||||
|
float b = mix(mix(hash13(i + vec3(0, 0, 1)), hash13(i + vec3(1, 0, 1)), u.x),
|
||||||
|
mix(hash13(i + vec3(0, 1, 1)), hash13(i + vec3(1, 1, 1)), u.x), u.y);
|
||||||
|
return mix(a, b, u.z);
|
||||||
|
}
|
||||||
|
float fbm2(vec2 p) {
|
||||||
|
float a = 0.5, s = 0.0;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
s += a * vnoise(p);
|
||||||
|
p = p * 2.03 + 17.1;
|
||||||
|
a *= 0.5;
|
||||||
|
}
|
||||||
|
return s / 0.9375;
|
||||||
|
}
|
||||||
|
// Interleaved gradient noise: cheap per-pixel dither for ray marching.
|
||||||
|
float ign(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- depth ----
|
||||||
|
float linearDepth(float d) {
|
||||||
|
float n = uCamParams.x, f = uCamParams.y;
|
||||||
|
float z = d * 2.0 - 1.0;
|
||||||
|
return 2.0 * n * f / (f + n - z * (f - n));
|
||||||
|
}
|
||||||
|
vec3 worldFromDepth(vec2 uv, float d) {
|
||||||
|
vec4 p = uInvViewProj * vec4(uv * 2.0 - 1.0, d * 2.0 - 1.0, 1.0);
|
||||||
|
return p.xyz / p.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- lighting ----
|
||||||
|
vec3 shadowCoord(vec3 wp) {
|
||||||
|
vec4 lc = uLightViewProj * vec4(wp, 1.0);
|
||||||
|
return lc.xyz / lc.w * 0.5 + 0.5;
|
||||||
|
}
|
||||||
|
float sunShadow(vec3 wp) {
|
||||||
|
vec3 p = shadowCoord(wp);
|
||||||
|
if (any(lessThan(p.xy, vec2(0.0))) || any(greaterThan(p.xyz, vec3(1.0)))) return 1.0;
|
||||||
|
vec2 o = vec2(uShadowParams.x * uShadowParams.y);
|
||||||
|
float s = 0.0;
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x)
|
||||||
|
s += texture(uShadowMap, vec3(p.xy + vec2(x, y) * o, p.z));
|
||||||
|
return s / 9.0;
|
||||||
|
}
|
||||||
|
float sunShadowFast(vec3 wp) {
|
||||||
|
vec3 p = shadowCoord(wp);
|
||||||
|
if (any(lessThan(p.xy, vec2(0.0))) || any(greaterThan(p.xyz, vec3(1.0)))) return 1.0;
|
||||||
|
return texture(uShadowMap, p);
|
||||||
|
}
|
||||||
|
float canopyPattern(vec3 wp, float scale, float coverage, float octaves) {
|
||||||
|
const float canopyHeight = 14.0;
|
||||||
|
vec3 L = uSunDir.xyz;
|
||||||
|
vec2 p = wp.xz + L.xz / max(L.y, 0.15) * (canopyHeight - wp.y);
|
||||||
|
p = p * scale + uTime * uCookie.z * vec2(1.0, 0.6);
|
||||||
|
float n = octaves > 1.5 ? fbm2(p) : vnoise(p) * 0.75 + vnoise(p * 2.7 + 9.1) * 0.25;
|
||||||
|
return smoothstep(coverage - 0.06, coverage + 0.06, n);
|
||||||
|
}
|
||||||
|
// Dappled light from an implied tree canopy high above the scene, projected
|
||||||
|
// along the sun direction onto surfaces.
|
||||||
|
float canopyCookie(vec3 wp) {
|
||||||
|
if (uCookie.x <= 0.0) return 1.0;
|
||||||
|
return mix(1.0, canopyPattern(wp, uCookie.y, uCookie.w, 4.0), uCookie.x);
|
||||||
|
}
|
||||||
|
// Sparse beams through the same implied canopy, used inside fog so sunlight
|
||||||
|
// gathers into distinct god ray shafts instead of a uniform glow.
|
||||||
|
float shaftCookie(vec3 wp) {
|
||||||
|
if (uShaft.x <= 0.0) return 1.0;
|
||||||
|
return mix(1.0, canopyPattern(wp, uShaft.z, uShaft.y, 1.0), uShaft.x);
|
||||||
|
}
|
||||||
|
float sunVisibility(vec3 wp) { return sunShadow(wp) * canopyCookie(wp); }
|
||||||
|
|
||||||
|
vec3 hemiAmbient(vec3 n) {
|
||||||
|
vec3 sky = mix(uSkyHorizon.rgb, uSkyZenith.rgb, 0.45);
|
||||||
|
return mix(uSkyGround.rgb, sky, n.y * 0.5 + 0.5) * uSkyZenith.w;
|
||||||
|
}
|
||||||
|
vec3 pointLights(vec3 wp, vec3 n) {
|
||||||
|
vec3 sum = vec3(0.0);
|
||||||
|
for (int i = 0; i < uLightCount.x; ++i) {
|
||||||
|
vec3 d = uLightPos[i].xyz - wp;
|
||||||
|
float dist = length(d);
|
||||||
|
float r = uLightPos[i].w;
|
||||||
|
if (dist >= r) continue;
|
||||||
|
float att = 1.0 - (dist * dist) / (r * r);
|
||||||
|
att *= att;
|
||||||
|
float ndl = clamp((dot(n, d / max(dist, 1e-4)) + 0.4) / 1.4, 0.0, 1.0);
|
||||||
|
sum += uLightColor[i].rgb * att * ndl;
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baked contact occlusion under scenery (tree bases, bushes, rocks). `height`
|
||||||
|
// is the shaded point's height above the ground it stands on: the darkening
|
||||||
|
// only applies near the ground.
|
||||||
|
float groundAO(vec3 wp, float height) {
|
||||||
|
if (uGroundAO.z <= 0.0) return 1.0;
|
||||||
|
float ao = texture(uGroundAOTex, (wp.xz - uGroundAO.xy) * uGroundAO.zw).r;
|
||||||
|
return mix(ao, 1.0, smoothstep(0.0, 1.2, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rain-soaked surfaces: darker albedo plus a sky/sun sheen on upward faces.
|
||||||
|
vec3 wetSurface(vec3 albedo, vec3 n, vec3 V, float vis, out vec3 sheen) {
|
||||||
|
float wet = uWeather.y * smoothstep(0.2, 0.8, n.y);
|
||||||
|
vec3 R = reflect(-V, n);
|
||||||
|
float F = 0.04 + 0.96 * pow(1.0 - max(dot(n, V), 0.0), 5.0);
|
||||||
|
sheen = wet * (uSkyHorizon.rgb * uSkyZenith.w * F * 0.6 +
|
||||||
|
uSunColor.rgb * uSunDir.w * pow(max(dot(R, uSunDir.xyz), 0.0), 60.0) * 0.5 * vis);
|
||||||
|
return albedo * mix(1.0, 0.68, uWeather.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
vec3 skyColor(vec3 dir) {
|
||||||
|
float h = dir.y;
|
||||||
|
vec3 c = mix(uSkyHorizon.rgb, uSkyZenith.rgb, pow(clamp(h, 0.0, 1.0), 0.55));
|
||||||
|
if (h < 0.0) c = mix(uSkyHorizon.rgb, uSkyGround.rgb, clamp(-h * 3.0, 0.0, 1.0));
|
||||||
|
float sd = max(dot(dir, uSunDir.xyz), 0.0);
|
||||||
|
if (h > 0.0) {
|
||||||
|
vec2 cp = dir.xz / (h + 0.12) * 1.3 + uTime * vec2(0.012, 0.004);
|
||||||
|
float cl = smoothstep(0.5 - 0.45 * uWeather.x, 0.85 - 0.3 * uWeather.x, fbm2(cp)); // overcast when raining
|
||||||
|
c = mix(c, uSunColor.rgb * (0.75 + 0.6 * sd), cl * 0.55 * smoothstep(0.0, 0.2, h));
|
||||||
|
}
|
||||||
|
c += uSunColor.rgb * (pow(sd, 900.0) * 40.0 + pow(sd, 10.0) * 0.3);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animated caustic web (after joltz0r's "water turbulence").
|
||||||
|
float caustics(vec2 uv, float time) {
|
||||||
|
vec2 p = mod(uv * 6.28318, 6.28318) - 250.0;
|
||||||
|
vec2 i = p;
|
||||||
|
float c = 1.0;
|
||||||
|
const float inten = 0.005;
|
||||||
|
for (int n = 0; n < 4; ++n) {
|
||||||
|
float t = time * (1.0 - (3.5 / float(n + 1)));
|
||||||
|
i = p + vec2(cos(t - i.x) + sin(t + i.y), sin(t - i.y) + cos(t + i.x));
|
||||||
|
c += 1.0 / length(vec2(p.x / (sin(i.x + t) / inten), p.y / (cos(i.y + t) / inten)));
|
||||||
|
}
|
||||||
|
c /= 4.0;
|
||||||
|
c = 1.17 - pow(c, 1.4);
|
||||||
|
return pow(abs(c), 8.0);
|
||||||
|
}
|
||||||
3
assets/shaders/depth.frag
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#version 450 core
|
||||||
|
// Depth-only pass (shadow map).
|
||||||
|
void main() {}
|
||||||
35
assets/shaders/dof_blur.frag
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#version 450 core
|
||||||
|
// Scatter-as-gather bokeh blur over a golden-angle disc at half resolution.
|
||||||
|
// Samples farther than the center cannot spread over it (keeps in-focus
|
||||||
|
// subjects from smearing into the background); nearer samples can (near
|
||||||
|
// field blur bleeds over sharp areas like a real lens).
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uHalf;
|
||||||
|
uniform float uMaxRadius;
|
||||||
|
uniform vec2 uTexel;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
const int SAMPLES = 64;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 c0 = texture(uHalf, vUV);
|
||||||
|
float cc = c0.a;
|
||||||
|
vec3 acc = c0.rgb;
|
||||||
|
float wsum = 1.0;
|
||||||
|
float nearCoverage = 0.0;
|
||||||
|
for (int i = 0; i < SAMPLES; ++i) {
|
||||||
|
float r = sqrt((float(i) + 0.5) / float(SAMPLES)) * uMaxRadius;
|
||||||
|
float th = float(i) * 2.39996323;
|
||||||
|
vec2 o = vec2(cos(th), sin(th)) * r;
|
||||||
|
vec4 s = textureLod(uHalf, vUV + o * uTexel, 0.0);
|
||||||
|
float cs = s.a;
|
||||||
|
float e = abs(cs);
|
||||||
|
if (cs > cc) e = min(e, abs(cc));
|
||||||
|
float w = clamp(e * uMaxRadius - r + 1.0, 0.0, 1.0);
|
||||||
|
acc += s.rgb * w;
|
||||||
|
wsum += w;
|
||||||
|
if (cs < 0.0) nearCoverage = max(nearCoverage, w * abs(cs));
|
||||||
|
}
|
||||||
|
fragColor = vec4(acc / wsum, max(abs(cc), nearCoverage));
|
||||||
|
}
|
||||||
12
assets/shaders/dof_common.glsl
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
uniform vec4 uDof; // focus distance, focus range, transition, max radius (half-res px)
|
||||||
|
uniform float uTiltShift;
|
||||||
|
|
||||||
|
// Signed CoC in [-1, 1]. Distance-based blur plus an optional screen-space
|
||||||
|
// tilt-shift band that exaggerates the miniature/diorama look.
|
||||||
|
float cocFromDepth(float linDepth, vec2 uv) {
|
||||||
|
float s = linDepth - uDof.x;
|
||||||
|
float a = smoothstep(uDof.y, uDof.y + uDof.z, abs(s));
|
||||||
|
float tilt = uTiltShift * smoothstep(0.18, 0.5, abs(uv.y - 0.5));
|
||||||
|
float sgn = abs(s) > 0.25 ? sign(s) : sign(uv.y - 0.5);
|
||||||
|
return max(a, tilt) * sgn;
|
||||||
|
}
|
||||||
17
assets/shaders/dof_composite.frag
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "dof_common.glsl"
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uSharp;
|
||||||
|
layout(binding = 1) uniform sampler2D uBlurred;
|
||||||
|
layout(binding = 2) uniform sampler2D uDepth;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 sharp = texture(uSharp, vUV).rgb;
|
||||||
|
vec4 blurred = texture(uBlurred, vUV);
|
||||||
|
float coc = abs(cocFromDepth(linearDepth(texture(uDepth, vUV).r), vUV));
|
||||||
|
float m = smoothstep(0.02, 0.35, max(coc, blurred.a));
|
||||||
|
fragColor = vec4(mix(sharp, blurred.rgb, m), 1.0);
|
||||||
|
}
|
||||||
15
assets/shaders/dof_prep.frag
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "dof_common.glsl"
|
||||||
|
// Half-res: downsampled color + signed circle of confusion (near < 0 < far).
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uColor;
|
||||||
|
layout(binding = 1) uniform sampler2D uDepth;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 c = texture(uColor, vUV).rgb; // bilinear = 2x2 box at half res
|
||||||
|
float coc = cocFromDepth(linearDepth(texture(uDepth, vUV).r), vUV);
|
||||||
|
fragColor = vec4(c, coc);
|
||||||
|
}
|
||||||
33
assets/shaders/fog_composite.frag
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Depth-aware upsample of the half-res volumetric buffer, applied to the scene.
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uColor;
|
||||||
|
layout(binding = 1) uniform sampler2D uDepth;
|
||||||
|
layout(binding = 2) uniform sampler2D uVolume;
|
||||||
|
uniform vec2 uVolumeTexel;
|
||||||
|
uniform int uEnabled;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 c = texture(uColor, vUV).rgb;
|
||||||
|
if (uEnabled == 0) {
|
||||||
|
fragColor = vec4(c, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float d0 = linearDepth(texture(uDepth, vUV).r);
|
||||||
|
vec4 acc = vec4(0.0);
|
||||||
|
float wsum = 0.0;
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x) {
|
||||||
|
vec2 uv = vUV + vec2(x, y) * uVolumeTexel;
|
||||||
|
float ds = linearDepth(texture(uDepth, uv).r);
|
||||||
|
float w = exp(-abs(ds - d0) / (0.04 * d0 + 0.05)) * (x == 0 && y == 0 ? 2.0 : 1.0);
|
||||||
|
acc += texture(uVolume, uv) * w;
|
||||||
|
wsum += w;
|
||||||
|
}
|
||||||
|
vec4 v = acc / max(wsum, 1e-4);
|
||||||
|
fragColor = vec4(c * v.a + v.rgb, 1.0);
|
||||||
|
}
|
||||||
8
assets/shaders/fullscreen.vert
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#version 450 core
|
||||||
|
// Fullscreen triangle from gl_VertexID; draw with 3 vertices, no buffers.
|
||||||
|
out vec2 vUV;
|
||||||
|
void main() {
|
||||||
|
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||||
|
vUV = p;
|
||||||
|
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
83
assets/shaders/mesh.frag
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// High-resolution 3D scenery (trees, bushes, rocks). Foliage uses wrap
|
||||||
|
// lighting on its rounded custom normals plus sun translucency.
|
||||||
|
|
||||||
|
in VS {
|
||||||
|
vec3 worldPos;
|
||||||
|
vec3 normal;
|
||||||
|
vec2 uv;
|
||||||
|
vec3 color;
|
||||||
|
float height;
|
||||||
|
} v;
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D uTex;
|
||||||
|
uniform vec4 uBaseColor;
|
||||||
|
uniform int uAlphaMask;
|
||||||
|
uniform float uCutoff;
|
||||||
|
uniform int uFoliage;
|
||||||
|
uniform int uOccluderFade; // 1 in the main view: dither away scenery hiding the player
|
||||||
|
uniform vec3 uEmissive; // linear HDR; multiplied by the emissive texture (unit 1)
|
||||||
|
layout(binding = 1) uniform sampler2D uEmissiveTex;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 tex = texture(uTex, v.uv);
|
||||||
|
if (uAlphaMask == 1) {
|
||||||
|
// Sharpen alpha by its screen-space rate of change: keeps leaf coverage
|
||||||
|
// stable through the mip chain instead of thinning out with distance.
|
||||||
|
float a = (tex.a - uCutoff) / max(fwidth(tex.a), 1e-4) + 0.5;
|
||||||
|
if (a < 0.5) discard;
|
||||||
|
}
|
||||||
|
if (uOccluderFade == 1 && uBlob.w > 0.0) {
|
||||||
|
vec4 pc = uViewProj * vec4(uBlob.xyz + vec3(0.0, 0.6, 0.0), 1.0);
|
||||||
|
vec2 ps = pc.xy / pc.w * 0.5 + 0.5;
|
||||||
|
vec2 d = (gl_FragCoord.xy * uScreen.zw - ps) * vec2(uScreen.x * uScreen.w, 1.0);
|
||||||
|
float fragDepth = (uViewProj * vec4(v.worldPos, 1.0)).w;
|
||||||
|
float fade = (1.0 - smoothstep(0.07, 0.15, length(d))) * step(fragDepth, pc.w - 1.0);
|
||||||
|
if (ign(gl_FragCoord.xy) < fade * 0.85) discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 wp = v.worldPos;
|
||||||
|
vec3 n = normalize(v.normal);
|
||||||
|
if (uFoliage == 0 && !gl_FrontFacing) n = -n;
|
||||||
|
vec3 L = uSunDir.xyz;
|
||||||
|
vec3 V = normalize(uCamPos.xyz - wp);
|
||||||
|
vec3 albedo = tex.rgb * uBaseColor.rgb * v.color;
|
||||||
|
float gao = groundAO(wp, v.height);
|
||||||
|
|
||||||
|
float ndl = dot(n, L);
|
||||||
|
float diffuse = uFoliage == 1 ? clamp((ndl + 0.45) / 1.45, 0.0, 1.0) : max(ndl, 0.0);
|
||||||
|
// Offset along the normal and toward the sun to avoid acne on thin cards.
|
||||||
|
float vis = sunShadow(wp + n * 0.06 + L * 0.08) * canopyCookie(wp);
|
||||||
|
if (uFoliage == 1) vis = mix(0.3, 1.0, vis); // light scatters through leaves: soft self-shadowing
|
||||||
|
vec3 sunRad = uSunColor.rgb * uSunDir.w;
|
||||||
|
|
||||||
|
vec3 toCam = cross(uBillboardRight.xyz, vec3(0.0, 1.0, 0.0));
|
||||||
|
vec3 amb = hemiAmbient(n) + uSkyHorizon.rgb * uSkyHorizon.w * max(dot(n, toCam), 0.0);
|
||||||
|
vec3 light = sunRad * diffuse * vis * mix(1.0, gao, 0.7) + amb * gao + pointLights(wp, n);
|
||||||
|
|
||||||
|
if (uFoliage == 1) {
|
||||||
|
// Backlit leaves glow; sunlit rims pick up a warm sheen.
|
||||||
|
float back = pow(max(dot(-V, L), 0.0), 4.0);
|
||||||
|
light += sunRad * back * vis * 0.35 * vec3(1.0, 1.05, 0.7);
|
||||||
|
float rim = pow(1.0 - max(dot(n, V), 0.0), 3.0) * max(ndl, 0.0);
|
||||||
|
light += sunRad * rim * vis * 0.25;
|
||||||
|
} else {
|
||||||
|
// Subtle sheen on rocks and bark.
|
||||||
|
vec3 H = normalize(L + V);
|
||||||
|
light += sunRad * pow(max(dot(n, H), 0.0), 24.0) * 0.06 * vis;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 sheen = vec3(0.0);
|
||||||
|
if (uWeather.y > 0.0) albedo = wetSurface(albedo, n, V, vis, sheen);
|
||||||
|
vec3 col = albedo * light + sheen * gao;
|
||||||
|
col += texture(uEmissiveTex, v.uv).rgb * uEmissive;
|
||||||
|
if (uBlob.w > 0.0) { // player's contact shadow
|
||||||
|
float d = length(wp.xz - uBlob.xz) / uBlob.w;
|
||||||
|
float near = 1.0 - smoothstep(0.0, 0.6, abs(wp.y - uBlob.y));
|
||||||
|
col *= 1.0 - 0.4 * (1.0 - smoothstep(0.3, 1.0, d)) * near;
|
||||||
|
}
|
||||||
|
fragColor = vec4(col, 1.0);
|
||||||
|
}
|
||||||
54
assets/shaders/mesh.vert
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 aPos;
|
||||||
|
layout(location = 1) in vec3 aNormal;
|
||||||
|
layout(location = 2) in vec2 aUV;
|
||||||
|
layout(location = 3) in vec4 aColor; // rgb = tint/AO, a = wind weight
|
||||||
|
layout(location = 4) in vec4 iPosYaw; // pivot xyz, yaw
|
||||||
|
layout(location = 5) in vec4 iScaleTint; // scale xyz, tint
|
||||||
|
|
||||||
|
uniform float uWindScale;
|
||||||
|
|
||||||
|
out VS {
|
||||||
|
vec3 worldPos;
|
||||||
|
vec3 normal;
|
||||||
|
vec2 uv;
|
||||||
|
vec3 color;
|
||||||
|
float height; // above the model's base (ground contact AO)
|
||||||
|
} o;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float c = cos(iPosYaw.w), s = sin(iPosYaw.w);
|
||||||
|
vec3 lp = aPos * iScaleTint.xyz;
|
||||||
|
vec3 p = vec3(c * lp.x + s * lp.z, lp.y, -s * lp.x + c * lp.z) + iPosYaw.xyz;
|
||||||
|
vec3 ln = aNormal / iScaleTint.xyz;
|
||||||
|
vec3 n = normalize(vec3(c * ln.x + s * ln.z, ln.y, -s * ln.x + c * ln.z));
|
||||||
|
|
||||||
|
float w = aColor.a * uWindScale;
|
||||||
|
if (w > 0.0) {
|
||||||
|
vec3 pivot = iPosYaw.xyz;
|
||||||
|
float ph = dot(pivot.xz, vec2(0.37, 0.61));
|
||||||
|
vec2 wdir = normalize(uWind.xy + 1e-5);
|
||||||
|
float gust = vnoise(pivot.xz * 0.12 - wdir * uTime * uWind.w * 0.6);
|
||||||
|
// Whole-crown sway grows with height; leaves add a fast, small flutter.
|
||||||
|
float sway = (sin(uTime * 1.1 + ph) * 0.5 + sin(uTime * 1.9 + ph * 1.3) * 0.25 + gust * 1.2) * uWind.z;
|
||||||
|
float h = max(lp.y, 0.0);
|
||||||
|
p.xz += wdir * sway * w * h * 0.35;
|
||||||
|
// Leaf flutter scales with wind strength, so storms visibly thrash the canopy.
|
||||||
|
p += n * sin(uTime * (3.0 + uWind.w * 2.0) + dot(aPos, vec3(7.1, 11.3, 5.7)) + ph) * 0.4 * uWind.z * w;
|
||||||
|
// Low foliage (bushes) parts around the player like the grass does.
|
||||||
|
vec3 toP = p - uInteractor.xyz;
|
||||||
|
float d = length(toP.xz);
|
||||||
|
float push = clamp(1.0 - d / (uInteractor.w * 1.3), 0.0, 1.0) * step(abs(toP.y), 1.2);
|
||||||
|
p.xz += (d > 1e-3 ? toP.xz / d : vec2(0.0)) * push * 0.3 * w;
|
||||||
|
}
|
||||||
|
|
||||||
|
o.worldPos = p;
|
||||||
|
o.normal = n;
|
||||||
|
o.uv = aUV;
|
||||||
|
o.color = aColor.rgb * iScaleTint.w;
|
||||||
|
o.height = lp.y;
|
||||||
|
gl_Position = uViewProj * vec4(p, 1.0);
|
||||||
|
gl_ClipDistance[0] = dot(vec4(p, 1.0), uClipPlane);
|
||||||
|
}
|
||||||
18
assets/shaders/mesh_depth.frag
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#version 450 core
|
||||||
|
// Shadow pass for 3D scenery: alpha-tested so leaves cast dappled shadows.
|
||||||
|
|
||||||
|
in VS {
|
||||||
|
vec3 worldPos;
|
||||||
|
vec3 normal;
|
||||||
|
vec2 uv;
|
||||||
|
vec3 color;
|
||||||
|
float height;
|
||||||
|
} v;
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D uTex;
|
||||||
|
uniform int uAlphaMask;
|
||||||
|
uniform float uCutoff;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (uAlphaMask == 1 && texture(uTex, v.uv).a < uCutoff) discard;
|
||||||
|
}
|
||||||
56
assets/shaders/particles.frag
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Premultiplied output: glows and fire write alpha 0 (pure additive), sparks
|
||||||
|
// and smoke write coverage (alpha-blended). Blend func is ONE, ONE_MINUS_SRC_ALPHA.
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
in vec4 vColor;
|
||||||
|
flat in int vKind;
|
||||||
|
in vec2 vLifeSeed;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 p = vUV * 2.0 - 1.0;
|
||||||
|
float r = length(p);
|
||||||
|
float life = vLifeSeed.x, seed = vLifeSeed.y;
|
||||||
|
|
||||||
|
if (vKind == 2) { // spark / droplet
|
||||||
|
float a = (1.0 - smoothstep(0.6, 1.0, r)) * vColor.a;
|
||||||
|
fragColor = vec4(vColor.rgb * a, a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vKind == 3) { // flame: teardrop eroded by scrolling noise, more as it ages
|
||||||
|
float taper = 1.0 + 0.7 * (p.y * 0.5 + 0.5);
|
||||||
|
float d = length(vec2(p.x * taper, p.y * 0.9 + 0.1));
|
||||||
|
vec2 q = vec2(p.x * 2.5, p.y * 2.0 - uTime * 3.5) + seed * 37.0;
|
||||||
|
float n = vnoise(q) * 0.6 + vnoise(q * 2.3 + 11.0) * 0.4;
|
||||||
|
float shape = 1.0 - smoothstep(0.45, 0.85, d + (n - 0.5) * (0.6 + 0.9 * life));
|
||||||
|
float core = 1.0 - smoothstep(0.0, 0.55, d);
|
||||||
|
float a = shape * vColor.a;
|
||||||
|
// Mostly additive, but each tongue also dims what is behind it a little,
|
||||||
|
// so flames stay saturated orange over bright daytime ground.
|
||||||
|
fragColor = vec4(vColor.rgb * a * (0.7 + 0.8 * core), a * 0.3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vKind == 4) { // ember
|
||||||
|
float core = exp(-r * r * 10.0);
|
||||||
|
fragColor = vec4(vColor.rgb * vColor.a * core, 0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vKind == 5) { // smoke puff
|
||||||
|
vec2 q = p * 1.6 + seed * 23.0 + vec2(0.0, -uTime * 0.3);
|
||||||
|
float n = vnoise(q) * 0.6 + vnoise(q * 2.1 + 5.0) * 0.4;
|
||||||
|
float a = (1.0 - smoothstep(0.2, 1.0, r + (n - 0.5) * 0.7)) * vColor.a;
|
||||||
|
fragColor = vec4(vColor.rgb * a, a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vKind == 6) { // fire glow halo
|
||||||
|
float g = exp(-r * r * 3.5) - 0.03;
|
||||||
|
fragColor = vec4(vColor.rgb * max(g, 0.0), 0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float core = 1.0 - smoothstep(0.0, 0.2, r);
|
||||||
|
float halo = exp(-r * r * 9.0) * 0.45;
|
||||||
|
fragColor = vec4(vColor.rgb * vColor.a * (core * 1.5 + halo), 0.0);
|
||||||
|
}
|
||||||
50
assets/shaders/particles.vert
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 iPosSize;
|
||||||
|
layout(location = 1) in vec4 iColor; // rgb, alpha
|
||||||
|
// x = kind (0 firefly, 1 mote, 2 spark, 3 flame, 4 ember, 5 smoke, 6 glow),
|
||||||
|
// y = age / life, z = per-particle random seed
|
||||||
|
layout(location = 2) in vec4 iParams;
|
||||||
|
|
||||||
|
out vec2 vUV;
|
||||||
|
out vec4 vColor;
|
||||||
|
flat out int vKind;
|
||||||
|
out vec2 vLifeSeed;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1)) - 0.5;
|
||||||
|
vec3 camRight = vec3(uView[0][0], uView[1][0], uView[2][0]);
|
||||||
|
vec3 camUp = vec3(uView[0][1], uView[1][1], uView[2][1]);
|
||||||
|
int kind = int(iParams.x + 0.5);
|
||||||
|
float seed = iParams.z;
|
||||||
|
|
||||||
|
vec2 offset = corner;
|
||||||
|
float extent = 5.0; // fireflies / motes need room for their halo
|
||||||
|
if (kind == 2 || kind == 4 || kind == 6) extent = 1.0;
|
||||||
|
if (kind == 3) { // flame tongues are taller than wide
|
||||||
|
extent = 1.0;
|
||||||
|
offset *= vec2(1.0, 1.7);
|
||||||
|
}
|
||||||
|
if (kind == 5) { // smoke puffs rotate slowly
|
||||||
|
extent = 1.0;
|
||||||
|
float a = seed * 6.2832 + iParams.y * 1.5;
|
||||||
|
offset = mat2(cos(a), sin(a), -sin(a), cos(a)) * offset;
|
||||||
|
}
|
||||||
|
vec3 c = iPosSize.xyz;
|
||||||
|
vec3 wp = c + (camRight * offset.x + camUp * offset.y) * iPosSize.w * extent;
|
||||||
|
|
||||||
|
vec4 col = iColor;
|
||||||
|
if (kind == 1) { // dust motes only sparkle inside sun shafts
|
||||||
|
float vis = sunShadowFast(c) * shaftCookie(c);
|
||||||
|
col.rgb *= uSunColor.rgb * uSunDir.w * 0.8;
|
||||||
|
col.a *= vis;
|
||||||
|
}
|
||||||
|
if (kind == 5) // smoke is lit by the sky and warmed by nearby fire light
|
||||||
|
col.rgb *= hemiAmbient(vec3(0, 1, 0)) * 2.5 + pointLights(c, vec3(0, -1, 0)) * 0.4;
|
||||||
|
vUV = corner + 0.5;
|
||||||
|
vColor = col;
|
||||||
|
vKind = kind;
|
||||||
|
vLifeSeed = iParams.yz;
|
||||||
|
gl_Position = uViewProj * vec4(wp, 1.0);
|
||||||
|
}
|
||||||
19
assets/shaders/pixelart.glsl
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Fragment-stage only (uses screen-space derivatives).
|
||||||
|
// "Sharp bilinear": crisp texel edges when magnified, antialiased boundaries
|
||||||
|
// and correct mip selection when minified (unlike GL_NEAREST shimmer).
|
||||||
|
vec4 sharpSample(sampler2D tex, vec2 uv) {
|
||||||
|
vec2 size = vec2(textureSize(tex, 0));
|
||||||
|
vec2 px = uv * size;
|
||||||
|
vec2 seam = floor(px + 0.5);
|
||||||
|
vec2 fw = max(fwidth(px), vec2(1e-5));
|
||||||
|
px = seam + clamp((px - seam) / fw, -0.5, 0.5);
|
||||||
|
return textureGrad(tex, px / size, dFdx(uv), dFdy(uv));
|
||||||
|
}
|
||||||
|
vec4 sharpSampleArray(sampler2DArray tex, vec2 uv, float layer) {
|
||||||
|
vec2 size = vec2(textureSize(tex, 0).xy);
|
||||||
|
vec2 px = uv * size;
|
||||||
|
vec2 seam = floor(px + 0.5);
|
||||||
|
vec2 fw = max(fwidth(px), vec2(1e-5));
|
||||||
|
px = seam + clamp((px - seam) / fw, -0.5, 0.5);
|
||||||
|
return textureGrad(tex, vec3(px / size, layer), dFdx(uv), dFdy(uv));
|
||||||
|
}
|
||||||
15
assets/shaders/rain.frag
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Thin, slightly bright streak lit by the overcast sky. Premultiplied alpha.
|
||||||
|
|
||||||
|
in vec2 vLocal;
|
||||||
|
in float vAlpha;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float across = 1.0 - abs(vLocal.x);
|
||||||
|
float along = (1.0 - vLocal.y) * smoothstep(0.0, 0.15, vLocal.y + 0.05); // bright head, fading tail
|
||||||
|
float a = across * across * along * vAlpha * 0.6;
|
||||||
|
vec3 c = hemiAmbient(vec3(0.0, 1.0, 0.0)) * 2.0 + uSunColor.rgb * uSunDir.w * 0.2;
|
||||||
|
fragColor = vec4(c * a, a);
|
||||||
|
}
|
||||||
43
assets/shaders/rain.vert
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Fully procedural rain: each instance is one streak in a box that follows
|
||||||
|
// the camera target. Drops are fixed in world space (the box wraps around
|
||||||
|
// them), fall at uWeather.z and slant with the wind.
|
||||||
|
|
||||||
|
uniform vec3 uRainCenter;
|
||||||
|
uniform vec2 uRainBox; // half extent (xz), height
|
||||||
|
|
||||||
|
out vec2 vLocal; // x across the streak (-1..1), y along it (0 = head)
|
||||||
|
out float vAlpha;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int id = gl_InstanceID;
|
||||||
|
vec3 h = vec3(hash12(vec2(float(id), 1.37)), hash12(vec2(float(id), 7.13)), hash12(vec2(float(id), 3.71)));
|
||||||
|
float E = uRainBox.x, H = uRainBox.y;
|
||||||
|
float speed = uWeather.z * (0.85 + 0.3 * h.x);
|
||||||
|
vec2 wdir = normalize(uWind.xy + 1e-5);
|
||||||
|
vec3 vel = vec3(wdir.x, 0.0, wdir.y) * uWind.z * 28.0 + vec3(0.0, -speed, 0.0);
|
||||||
|
|
||||||
|
float age = fract(h.y + uTime * speed / H); // 0 = top of the box
|
||||||
|
float tFall = age * H / speed;
|
||||||
|
vec3 p;
|
||||||
|
p.y = uRainCenter.y + H * 0.75 - age * H;
|
||||||
|
vec2 base = h.xz * 2.0 * E + vel.xz * tFall;
|
||||||
|
p.xz = uRainCenter.xz + mod(base - uRainCenter.xz + E, 2.0 * E) - E;
|
||||||
|
|
||||||
|
vec3 dir = normalize(vel);
|
||||||
|
float len = speed * 0.032;
|
||||||
|
vec3 toCam = uCamPos.xyz - p;
|
||||||
|
float dist = length(toCam);
|
||||||
|
vec3 side = normalize(cross(dir, toCam / max(dist, 1e-3)));
|
||||||
|
float width = 0.008 + dist * 0.0006; // never thinner than ~1 px
|
||||||
|
|
||||||
|
vec2 corner = vec2(float(gl_VertexID & 1) * 2.0 - 1.0, float(gl_VertexID >> 1));
|
||||||
|
vec3 wp = p - dir * len * corner.y + side * width * corner.x;
|
||||||
|
|
||||||
|
vLocal = corner;
|
||||||
|
// Fade near the camera (big blurry drops) and at the box edges.
|
||||||
|
vec2 rel = abs(p.xz - uRainCenter.xz) / E;
|
||||||
|
vAlpha = smoothstep(1.5, 5.0, dist) * (1.0 - smoothstep(0.75, 1.0, max(rel.x, rel.y))) * (0.6 + 0.4 * h.z);
|
||||||
|
gl_Position = uViewProj * vec4(wp, 1.0);
|
||||||
|
}
|
||||||
11
assets/shaders/sky.frag
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 far = worldFromDepth(vUV, 1.0);
|
||||||
|
vec3 dir = normalize(far - uCamPos.xyz);
|
||||||
|
fragColor = vec4(skyColor(dir), 1.0);
|
||||||
|
}
|
||||||
55
assets/shaders/ssao.frag
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Half-res world-space hemisphere SSAO. Normals are reconstructed from depth
|
||||||
|
// (picking the flatter neighbour per axis to avoid smearing across edges).
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uDepth;
|
||||||
|
uniform float uRadius;
|
||||||
|
uniform float uIntensity;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
const int SAMPLES = 14;
|
||||||
|
|
||||||
|
vec3 posAt(vec2 uv) { return worldFromDepth(uv, texture(uDepth, uv).r); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float d = texture(uDepth, vUV).r;
|
||||||
|
if (d >= 1.0) {
|
||||||
|
fragColor = vec4(1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vec3 P = worldFromDepth(vUV, d);
|
||||||
|
vec2 t = 1.0 / vec2(textureSize(uDepth, 0));
|
||||||
|
vec3 px = posAt(vUV + vec2(t.x, 0)), nx = posAt(vUV - vec2(t.x, 0));
|
||||||
|
vec3 py = posAt(vUV + vec2(0, t.y)), ny = posAt(vUV - vec2(0, t.y));
|
||||||
|
vec3 dx = length(px - P) < length(P - nx) ? px - P : P - nx;
|
||||||
|
vec3 dy = length(py - P) < length(P - ny) ? py - P : P - ny;
|
||||||
|
vec3 N = normalize(cross(dx, dy));
|
||||||
|
if (dot(N, uCamPos.xyz - P) < 0.0) N = -N;
|
||||||
|
|
||||||
|
float rot = ign(gl_FragCoord.xy) * 6.2831853;
|
||||||
|
vec3 T = normalize(cross(N, abs(N.y) < 0.9 ? vec3(0, 1, 0) : vec3(1, 0, 0)));
|
||||||
|
vec3 B = cross(N, T);
|
||||||
|
float centerLin = linearDepth(d);
|
||||||
|
// Larger radius further away keeps the effect stable on screen.
|
||||||
|
float radius = uRadius * (0.8 + centerLin * 0.01);
|
||||||
|
|
||||||
|
float occ = 0.0;
|
||||||
|
for (int i = 0; i < SAMPLES; ++i) {
|
||||||
|
float fi = (float(i) + 0.5) / float(SAMPLES);
|
||||||
|
float a = float(i) * 2.399963 + rot;
|
||||||
|
float r = sqrt(fi);
|
||||||
|
float z = sqrt(max(0.0, 1.0 - r * r));
|
||||||
|
vec3 k = (T * cos(a) * r + B * sin(a) * r + N * z) * mix(0.15, 1.0, fi * fi);
|
||||||
|
vec3 S = P + N * 0.03 + k * radius;
|
||||||
|
vec4 clip = uViewProj * vec4(S, 1.0);
|
||||||
|
vec2 suv = clip.xy / clip.w * 0.5 + 0.5;
|
||||||
|
if (any(lessThan(suv, vec2(0.0))) || any(greaterThan(suv, vec2(1.0)))) continue;
|
||||||
|
float sceneLin = linearDepth(texture(uDepth, suv).r);
|
||||||
|
float range = smoothstep(0.0, 1.0, radius / max(abs(centerLin - sceneLin), 1e-3));
|
||||||
|
occ += step(sceneLin, clip.w - 0.02) * range;
|
||||||
|
}
|
||||||
|
float ao = 1.0 - occ / float(SAMPLES);
|
||||||
|
fragColor = vec4(pow(clamp(ao, 0.0, 1.0), uIntensity * 1.6));
|
||||||
|
}
|
||||||
29
assets/shaders/ssao_apply.frag
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Depth-aware upsample of the AO buffer; output is multiplied into the HDR
|
||||||
|
// target by the blend unit (DST_COLOR, ZERO).
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uAo;
|
||||||
|
layout(binding = 1) uniform sampler2D uDepth;
|
||||||
|
uniform vec2 uAoTexel;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float raw = texture(uDepth, vUV).r;
|
||||||
|
if (raw >= 1.0) {
|
||||||
|
fragColor = vec4(1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float d0 = linearDepth(raw);
|
||||||
|
float sum = 0.0, wsum = 0.0;
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x) {
|
||||||
|
vec2 uv = vUV + vec2(x, y) * uAoTexel;
|
||||||
|
float d = linearDepth(texture(uDepth, uv).r);
|
||||||
|
float w = exp(-abs(d - d0) / (0.03 * d0 + 0.05));
|
||||||
|
sum += texture(uAo, uv).r * w;
|
||||||
|
wsum += w;
|
||||||
|
}
|
||||||
|
fragColor = vec4(vec3(sum / max(wsum, 1e-4)), 1.0);
|
||||||
|
}
|
||||||
22
assets/shaders/ssao_blur.frag
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Separable depth-aware blur of the half-res AO buffer.
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uAo;
|
||||||
|
layout(binding = 1) uniform sampler2D uDepth;
|
||||||
|
uniform vec2 uDir; // texel step along the blur axis
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float d0 = linearDepth(texture(uDepth, vUV).r);
|
||||||
|
float sum = 0.0, wsum = 0.0;
|
||||||
|
for (int i = -4; i <= 4; ++i) {
|
||||||
|
vec2 uv = vUV + uDir * float(i);
|
||||||
|
float d = linearDepth(texture(uDepth, uv).r);
|
||||||
|
float w = exp(-float(i * i) / 12.0) * exp(-abs(d - d0) / (0.03 * d0 + 0.05));
|
||||||
|
sum += texture(uAo, uv).r * w;
|
||||||
|
wsum += w;
|
||||||
|
}
|
||||||
|
fragColor = vec4(sum / max(wsum, 1e-4));
|
||||||
|
}
|
||||||
96
assets/shaders/terrain.frag
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
#include "pixelart.glsl"
|
||||||
|
|
||||||
|
in VS {
|
||||||
|
vec3 worldPos;
|
||||||
|
vec3 normal;
|
||||||
|
vec2 uv;
|
||||||
|
flat float layer;
|
||||||
|
float ao;
|
||||||
|
flat float kind;
|
||||||
|
} v;
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2DArray uTerrainTex;
|
||||||
|
layout(binding = 1) uniform usampler2D uMatMap;
|
||||||
|
uniform ivec2 uMapSize;
|
||||||
|
uniform ivec4 uMatTop[8]; // topFirst, topCount, accentFirst, accentCount
|
||||||
|
uniform float uMatAccent[8]; // accent chance
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
// Picks the material of the pixel-art texel at `wp`. Tile borders are jittered
|
||||||
|
// with low-frequency noise plus chunky per-4px offsets so transitions look
|
||||||
|
// hand-drawn instead of grid aligned.
|
||||||
|
float topLayer(vec3 wp) {
|
||||||
|
float texels = float(textureSize(uTerrainTex, 0).x);
|
||||||
|
vec2 sp = (floor(wp.xz * texels) + 0.5) / texels;
|
||||||
|
vec2 j = vec2(vnoise(sp * 1.7), vnoise(sp * 1.7 + vec2(31.7, 11.3))) - 0.5;
|
||||||
|
vec2 chunk = floor(sp * texels / 4.0);
|
||||||
|
j += (vec2(hash12(chunk), hash12(chunk + 7.7)) - 0.5) * 0.3;
|
||||||
|
ivec2 cell = clamp(ivec2(floor(sp + j * 0.55)), ivec2(0), uMapSize - 1);
|
||||||
|
uint m = min(texelFetch(uMatMap, cell, 0).r, 7u);
|
||||||
|
ivec4 mt = uMatTop[m];
|
||||||
|
vec2 c = vec2(cell);
|
||||||
|
int layer = mt.x + int(hash12(c + 0.37) * float(mt.y)) % max(mt.y, 1);
|
||||||
|
if (mt.w > 0 && hash12(c * 1.37 + 5.1) < uMatAccent[m])
|
||||||
|
layer = mt.z + int(hash12(c + 9.3) * float(mt.w)) % mt.w;
|
||||||
|
return float(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 wp = v.worldPos;
|
||||||
|
vec3 n = normalize(v.normal);
|
||||||
|
vec4 albedo;
|
||||||
|
if (v.kind < 0.5) {
|
||||||
|
albedo = sharpSampleArray(uTerrainTex, wp.xz, topLayer(wp));
|
||||||
|
} else {
|
||||||
|
albedo = sharpSampleArray(uTerrainTex, v.uv, v.layer);
|
||||||
|
if (v.kind > 1.5) {
|
||||||
|
// Ragged overhang strip (e.g. grass hanging over a cliff edge).
|
||||||
|
float texels = float(textureSize(uTerrainTex, 0).x);
|
||||||
|
float px = floor(v.uv.x * texels);
|
||||||
|
float lip = 2.0 + floor(hash12(vec2(px, v.layer)) * 2.5) + floor(vnoise(vec2(px * 0.3, 3.0)) * 3.0);
|
||||||
|
float row = v.uv.y * texels;
|
||||||
|
if (row < lip) {
|
||||||
|
albedo = sharpSampleArray(uTerrainTex, vec2(v.uv.x, wp.y), v.kind - 2.0);
|
||||||
|
albedo.rgb *= mix(1.0, 0.7, row / lip);
|
||||||
|
} else if (row < lip + 1.0) {
|
||||||
|
albedo.rgb *= 0.55; // dark contact line under the lip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 L = uSunDir.xyz;
|
||||||
|
float ndl = max(dot(n, L), 0.0);
|
||||||
|
float vis = sunVisibility(wp + n * 0.03);
|
||||||
|
vec3 sun = uSunColor.rgb * uSunDir.w * ndl * vis;
|
||||||
|
// Camera-side fill keeps cliff faces readable when the sun is behind them.
|
||||||
|
vec3 toCam = cross(uBillboardRight.xyz, vec3(0.0, 1.0, 0.0));
|
||||||
|
vec3 amb = (hemiAmbient(n) + uSkyHorizon.rgb * uSkyHorizon.w * max(dot(n, toCam), 0.0)) * v.ao;
|
||||||
|
float gao = groundAO(wp, v.kind < 0.5 ? 0.0 : 0.6);
|
||||||
|
vec3 sheen = vec3(0.0);
|
||||||
|
vec3 base = albedo.rgb;
|
||||||
|
if (uWeather.y > 0.0 && wp.y > uWaterParams.x)
|
||||||
|
base = wetSurface(base, n, normalize(uCamPos.xyz - wp), vis, sheen);
|
||||||
|
// Contact AO dims ambient fully and direct light partially (reads as a soft contact shadow).
|
||||||
|
vec3 col = base * (sun * mix(1.0, gao, 0.7) + amb * gao + pointLights(wp, n)) + sheen * gao;
|
||||||
|
|
||||||
|
// Caustics on underwater surfaces, projected along the sun direction.
|
||||||
|
if (uWaterParams.z > 0.5 && wp.y < uWaterParams.x) {
|
||||||
|
float depth = uWaterParams.x - wp.y;
|
||||||
|
vec2 cp = wp.xz + L.xz / max(L.y, 0.2) * depth;
|
||||||
|
float c = caustics(cp * 0.33, uTime * 0.55);
|
||||||
|
float vSurf = sunVisibility(wp + L * (depth / max(L.y, 0.2)));
|
||||||
|
col += albedo.rgb * uSunColor.rgb * uSunDir.w * c * uWaterParams.y * vSurf *
|
||||||
|
exp(-depth * 0.7) * max(n.y, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft contact shadow under the player sprite.
|
||||||
|
if (uBlob.w > 0.0) {
|
||||||
|
float d = length(wp.xz - uBlob.xz) / uBlob.w;
|
||||||
|
float onFloor = 1.0 - smoothstep(0.2, 0.6, abs(wp.y - uBlob.y));
|
||||||
|
col *= 1.0 - 0.5 * (1.0 - smoothstep(0.3, 1.0, d)) * onFloor;
|
||||||
|
}
|
||||||
|
fragColor = vec4(col, 1.0);
|
||||||
|
}
|
||||||
27
assets/shaders/terrain.vert
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 aPos;
|
||||||
|
layout(location = 1) in vec3 aNormal;
|
||||||
|
layout(location = 2) in vec2 aUV;
|
||||||
|
layout(location = 3) in vec3 aMisc; // layer, ao, kind
|
||||||
|
|
||||||
|
out VS {
|
||||||
|
vec3 worldPos;
|
||||||
|
vec3 normal;
|
||||||
|
vec2 uv;
|
||||||
|
flat float layer;
|
||||||
|
float ao;
|
||||||
|
flat float kind;
|
||||||
|
} o;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
o.worldPos = aPos;
|
||||||
|
o.normal = aNormal;
|
||||||
|
o.uv = aUV;
|
||||||
|
o.layer = aMisc.x;
|
||||||
|
o.ao = aMisc.y;
|
||||||
|
o.kind = aMisc.z;
|
||||||
|
gl_Position = uViewProj * vec4(aPos, 1.0);
|
||||||
|
gl_ClipDistance[0] = dot(vec4(aPos, 1.0), uClipPlane);
|
||||||
|
}
|
||||||
63
assets/shaders/tonemap.frag
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#version 450 core
|
||||||
|
// Bloom mix, color grading, ACES tonemap, vignette, chromatic aberration, grain.
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uHdr;
|
||||||
|
layout(binding = 1) uniform sampler2D uBloom;
|
||||||
|
|
||||||
|
uniform float uBloomStrength;
|
||||||
|
uniform float uBloomNorm;
|
||||||
|
uniform float uExposure;
|
||||||
|
uniform float uContrast;
|
||||||
|
uniform float uSaturation;
|
||||||
|
uniform vec3 uLift;
|
||||||
|
uniform vec3 uGain;
|
||||||
|
uniform float uVignette;
|
||||||
|
uniform float uChromatic;
|
||||||
|
uniform float uGrain;
|
||||||
|
uniform float uTimeSeed;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
vec3 acesFitted(vec3 x) {
|
||||||
|
const mat3 inM = mat3(0.59719, 0.07600, 0.02840, 0.35458, 0.90834, 0.13383, 0.04823, 0.01566, 0.83777);
|
||||||
|
const mat3 outM = mat3(1.60475, -0.10208, -0.00327, -0.53108, 1.10813, -0.07276, -0.07367, -0.00605, 1.07602);
|
||||||
|
vec3 v = inM * x;
|
||||||
|
vec3 a = v * (v + 0.0245786) - 0.000090537;
|
||||||
|
vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
|
||||||
|
return clamp(outM * (a / b), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hash12(vec2 p) {
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||||
|
p3 += dot(p3, p3.yzx + 33.33);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 dir = vUV - 0.5;
|
||||||
|
vec3 c;
|
||||||
|
c.r = texture(uHdr, vUV - dir * uChromatic * 2.0).r;
|
||||||
|
c.g = texture(uHdr, vUV).g;
|
||||||
|
c.b = texture(uHdr, vUV + dir * uChromatic * 2.0).b;
|
||||||
|
vec3 bloom = texture(uBloom, vUV).rgb * uBloomNorm;
|
||||||
|
c = mix(c, bloom, uBloomStrength);
|
||||||
|
c *= uExposure;
|
||||||
|
|
||||||
|
// Contrast in log space around middle grey, then saturation.
|
||||||
|
const float mid = 0.18;
|
||||||
|
c = exp2((log2(max(c, vec3(1e-6))) - log2(mid)) * uContrast + log2(mid));
|
||||||
|
float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
c = max(mix(vec3(l), c, uSaturation), 0.0);
|
||||||
|
c *= uGain;
|
||||||
|
|
||||||
|
c = acesFitted(c);
|
||||||
|
c += uLift * (1.0 - c);
|
||||||
|
|
||||||
|
float v = 1.0 - smoothstep(0.25, 1.1, length(dir * vec2(1.25, 1.0)) * (0.6 + uVignette));
|
||||||
|
c *= mix(1.0, v, clamp(uVignette * 1.6, 0.0, 1.0));
|
||||||
|
|
||||||
|
c = pow(c, vec3(1.0 / 2.2));
|
||||||
|
c += (hash12(gl_FragCoord.xy + uTimeSeed) - 0.5) * uGrain;
|
||||||
|
fragColor = vec4(c, 1.0);
|
||||||
|
}
|
||||||
66
assets/shaders/volumetric.frag
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
// Half-resolution ray march through height fog. Each step samples the sun
|
||||||
|
// shadow map and canopy cookie, producing physically shadowed light shafts
|
||||||
|
// (god rays). Output: rgb = in-scattered light, a = transmittance.
|
||||||
|
|
||||||
|
in vec2 vUV;
|
||||||
|
layout(binding = 0) uniform sampler2D uDepth;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
float phaseHG(float c, float g) {
|
||||||
|
float g2 = g * g;
|
||||||
|
return (1.0 - g2) / (4.0 * PI * pow(1.0 + g2 - 2.0 * g * c, 1.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
float fogDensity(vec3 p) {
|
||||||
|
float h = exp(-max(p.y - uFogParams.y, 0.0) * uFogParams.x);
|
||||||
|
vec3 drift = vec3(uWind.x, 0.05, uWind.y) * uTime * 0.35;
|
||||||
|
float n = vnoise3(p * 0.16 + drift) * 0.65 + vnoise3(p * 0.45 - drift * 1.7) * 0.35;
|
||||||
|
n = mix(1.0, smoothstep(0.2, 0.8, n) * 1.8, uFogParams.z);
|
||||||
|
return uFogColor.w * h * n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float d = texture(uDepth, vUV).r;
|
||||||
|
vec3 wp = worldFromDepth(vUV, d);
|
||||||
|
vec3 ro = uCamPos.xyz;
|
||||||
|
vec3 rd = wp - ro;
|
||||||
|
float dist = length(rd);
|
||||||
|
rd /= dist;
|
||||||
|
dist = min(dist, uFogParams2.y);
|
||||||
|
|
||||||
|
int steps = int(uFogParams2.z);
|
||||||
|
float stepLen = dist / float(steps);
|
||||||
|
float jitter = ign(gl_FragCoord.xy);
|
||||||
|
|
||||||
|
float cosT = dot(rd, uSunDir.xyz);
|
||||||
|
// Dual lobe: strong forward scattering + a little back/side so shafts
|
||||||
|
// still read when looking away from the sun. Normalized: isotropic = 1.
|
||||||
|
float phase = (0.75 * phaseHG(cosT, uFogParams.w) + 0.25 * phaseHG(cosT, -0.25)) * 4.0 * PI;
|
||||||
|
vec3 sunL = uSunColor.rgb * uSunDir.w * uFogParams2.x * phase;
|
||||||
|
vec3 ambL = uFogColor.rgb * uSkyZenith.w;
|
||||||
|
|
||||||
|
vec3 scatter = vec3(0.0);
|
||||||
|
float T = 1.0;
|
||||||
|
for (int i = 0; i < steps; ++i) {
|
||||||
|
vec3 p = ro + rd * ((float(i) + jitter) * stepLen);
|
||||||
|
float dens = fogDensity(p);
|
||||||
|
if (dens < 1e-5) continue;
|
||||||
|
// Sparse beam mask: most sun scattering happens inside distinct shafts.
|
||||||
|
float vis = sunShadowFast(p) * shaftCookie(p);
|
||||||
|
vec3 S = (sunL * vis + ambL) * dens;
|
||||||
|
for (int l = 0; l < uLightCount.y; ++l) { // warm haze around fires
|
||||||
|
vec3 dl = uLightPos[l].xyz - p;
|
||||||
|
float r = uLightPos[l].w;
|
||||||
|
float att = max(1.0 - dot(dl, dl) / (r * r), 0.0);
|
||||||
|
att *= att;
|
||||||
|
S += uLightColor[l].rgb * att * att * dens * 0.35;
|
||||||
|
}
|
||||||
|
float Ts = exp(-dens * stepLen);
|
||||||
|
scatter += T * S * (1.0 - Ts) / dens; // energy-conserving step integral
|
||||||
|
T *= Ts;
|
||||||
|
if (T < 0.01) break;
|
||||||
|
}
|
||||||
|
fragColor = vec4(scatter, T);
|
||||||
|
}
|
||||||
177
assets/shaders/water.frag
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
in vec3 vWorld;
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D uReflection;
|
||||||
|
layout(binding = 1) uniform sampler2D uSceneColor;
|
||||||
|
layout(binding = 2) uniform sampler2D uSceneDepth;
|
||||||
|
layout(binding = 3) uniform sampler2D uShoreDist;
|
||||||
|
|
||||||
|
uniform vec4 uShoreRect; // minX, minZ, 1/sizeX, 1/sizeZ
|
||||||
|
uniform vec3 uShallow;
|
||||||
|
uniform vec3 uDeep;
|
||||||
|
uniform vec3 uAbsorb;
|
||||||
|
uniform float uClarity;
|
||||||
|
uniform float uWaveScale;
|
||||||
|
uniform float uWaveStrength;
|
||||||
|
uniform float uWaveSpeed;
|
||||||
|
uniform float uRefraction;
|
||||||
|
uniform float uReflDistort;
|
||||||
|
uniform float uSpecular;
|
||||||
|
uniform float uGlitter;
|
||||||
|
uniform float uFoamWidth;
|
||||||
|
uniform float uFoamIntensity;
|
||||||
|
uniform int uHasReflection;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
// Value noise with analytic derivatives (iq).
|
||||||
|
vec3 noised(vec2 x) {
|
||||||
|
vec2 i = floor(x), f = fract(x);
|
||||||
|
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
||||||
|
vec2 du = 30.0 * f * f * (f * (f - 2.0) + 1.0);
|
||||||
|
float a = hash12(i), b = hash12(i + vec2(1, 0)), c = hash12(i + vec2(0, 1)), d = hash12(i + vec2(1, 1));
|
||||||
|
float k1 = b - a, k2 = c - a, k4 = a - b - c + d;
|
||||||
|
return vec3(a + k1 * u.x + k2 * u.y + k4 * u.x * u.y, du * vec2(k1 + k4 * u.y, k2 + k4 * u.x));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twinkling point highlights: one wandering point per cell, each with its own
|
||||||
|
// blink rhythm. `r` is the point radius in cell units; it never shrinks below
|
||||||
|
// ~1.5 px so sparkles stay stable instead of aliasing.
|
||||||
|
float glitter(vec2 p, float t, float r) {
|
||||||
|
vec2 cell = floor(p), f = fract(p);
|
||||||
|
float px = fwidth(p.x) * 1.5;
|
||||||
|
float rr = max(r, px);
|
||||||
|
float energy = r * r / (rr * rr); // keep brightness constant when widened
|
||||||
|
float s = 0.0;
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x) {
|
||||||
|
vec2 id = cell + vec2(x, y);
|
||||||
|
vec2 h = vec2(hash12(id), hash12(id + 19.19));
|
||||||
|
vec2 pt = vec2(x, y) + 0.5 + 0.35 * sin(t * (0.4 + h * 0.6) + h.yx * 6.2832);
|
||||||
|
float blink = pow(max(sin(t * (1.5 + 3.0 * h.x) + h.y * 40.0), 0.0), 6.0);
|
||||||
|
s += (1.0 - smoothstep(rr * 0.4, rr, length(f - pt))) * blink;
|
||||||
|
}
|
||||||
|
return s * energy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expanding rings from raindrops: one drop per cell per cycle, random phase.
|
||||||
|
// Returns the surface gradient contribution.
|
||||||
|
vec2 rainRipples(vec2 p, float t) {
|
||||||
|
vec2 cell = floor(p), f = fract(p);
|
||||||
|
vec2 grad = vec2(0.0);
|
||||||
|
for (int y = -1; y <= 1; ++y)
|
||||||
|
for (int x = -1; x <= 1; ++x) {
|
||||||
|
vec2 id = cell + vec2(x, y);
|
||||||
|
vec2 c = vec2(x, y) + vec2(hash12(id), hash12(id + 5.3)) * 0.8 + 0.1;
|
||||||
|
float phase = fract(t * 1.3 + hash12(id + 11.1));
|
||||||
|
vec2 d = f - c;
|
||||||
|
float r = length(d);
|
||||||
|
float front = phase * 0.9; // ring radius in cell units
|
||||||
|
float ring = sin((r - front) * 38.0) * exp(-(r - front) * (r - front) * 120.0) * (1.0 - phase);
|
||||||
|
grad += (r > 1e-3 ? d / r : vec2(0.0)) * ring;
|
||||||
|
}
|
||||||
|
return grad;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns (normal.xyz, normalized height) of layered drifting wavelets.
|
||||||
|
vec4 waterSurface(vec2 p, float t, float shoreDist) {
|
||||||
|
vec2 rain = uWeather.x > 0.0
|
||||||
|
? (rainRipples(p * 2.4, t) + rainRipples(p * 3.7 + 9.0, t * 1.17)) * 0.3 * uWeather.x
|
||||||
|
: vec2(0.0);
|
||||||
|
vec2 grad = vec2(0.0);
|
||||||
|
float h = 0.0, amp = 1.0, freq = uWaveScale, norm = 0.0;
|
||||||
|
const mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
|
||||||
|
vec2 dir = vec2(1.0, 0.35);
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
vec3 n = noised(p * freq + dir * t * uWaveSpeed * (1.0 + float(i) * 0.4));
|
||||||
|
h += n.x * amp;
|
||||||
|
norm += amp;
|
||||||
|
grad += n.yz * amp * freq;
|
||||||
|
p = rot * p;
|
||||||
|
dir = rot * dir;
|
||||||
|
amp *= 0.5;
|
||||||
|
freq *= 1.85;
|
||||||
|
}
|
||||||
|
grad *= uWaveStrength * 0.09;
|
||||||
|
// Ripples rolling in toward the shore.
|
||||||
|
float ring = sin(shoreDist * 9.0 + t * 2.4) * exp(-shoreDist * 1.6) * 0.06 * uWaveStrength;
|
||||||
|
grad += vec2(ring) + rain;
|
||||||
|
return vec4(normalize(vec3(-grad.x, 1.0, -grad.y)), h / norm);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 wp = vWorld;
|
||||||
|
vec3 V = normalize(uCamPos.xyz - wp);
|
||||||
|
float t = uTime;
|
||||||
|
vec2 suv = gl_FragCoord.xy * uScreen.zw;
|
||||||
|
float shoreDist = texture(uShoreDist, (wp.xz - uShoreRect.xy) * uShoreRect.zw).r;
|
||||||
|
vec4 surf = waterSurface(wp.xz, t, shoreDist);
|
||||||
|
vec3 n = surf.xyz;
|
||||||
|
|
||||||
|
// Bottom under this pixel.
|
||||||
|
vec3 sceneP = worldFromDepth(suv, texture(uSceneDepth, suv).r);
|
||||||
|
float depthV = max(wp.y - sceneP.y, 0.0);
|
||||||
|
|
||||||
|
// Refraction: offset by the surface normal, scaled down in the shallows.
|
||||||
|
vec2 ruv = suv + n.xz * uRefraction * clamp(depthV * 2.5, 0.0, 1.0);
|
||||||
|
vec3 rP = worldFromDepth(ruv, texture(uSceneDepth, ruv).r);
|
||||||
|
if (rP.y > wp.y + 0.01) { // refracted sample is above water: fall back
|
||||||
|
ruv = suv;
|
||||||
|
rP = sceneP;
|
||||||
|
}
|
||||||
|
vec3 refr = texture(uSceneColor, ruv).rgb;
|
||||||
|
float rDepth = max(wp.y - rP.y, 0.0);
|
||||||
|
float path = length(rP - wp) + rDepth;
|
||||||
|
vec3 transmittance = exp(-uAbsorb * path / max(uClarity, 0.05));
|
||||||
|
|
||||||
|
float sunVis = sunVisibility(wp);
|
||||||
|
vec3 L = uSunDir.xyz;
|
||||||
|
vec3 sunRad = uSunColor.rgb * uSunDir.w;
|
||||||
|
vec3 lightIn = sunRad * max(L.y, 0.0) * mix(0.35, 1.0, sunVis) + hemiAmbient(vec3(0, 1, 0));
|
||||||
|
vec3 body = mix(uShallow, uDeep, 1.0 - exp(-rDepth * 0.8)) * lightIn * 0.65;
|
||||||
|
vec3 under = refr * transmittance + body * (1.0 - transmittance);
|
||||||
|
|
||||||
|
// Reflection: planar reflection texture shares screen mapping with the main view.
|
||||||
|
vec3 R = reflect(-V, n);
|
||||||
|
vec3 refl = uHasReflection == 1 ? texture(uReflection, suv + n.xz * uReflDistort).rgb : skyColor(R);
|
||||||
|
refl *= mix(vec3(1.0), normalize(uShallow + 1e-3) * 1.6, 0.35); // water tints what it reflects
|
||||||
|
float ndv = max(dot(n, V), 0.0);
|
||||||
|
float F = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);
|
||||||
|
F = mix(F, 1.0, 0.06); // a hint of reflection even looking straight down
|
||||||
|
vec3 col = mix(under, refl, F);
|
||||||
|
|
||||||
|
// Sky-lit wave crests: stylized bright streaks that keep the surface alive
|
||||||
|
// even when the sun's reflection is out of view.
|
||||||
|
float crest = smoothstep(0.58, 0.78, surf.w);
|
||||||
|
col += skyColor(R) * crest * 0.10 * (0.5 + 0.5 * sunVis);
|
||||||
|
|
||||||
|
// Sun glints: tight core + wide sheen (HDR, drives bloom). Shadowed water
|
||||||
|
// still catches some sky glints so sparkles don't vanish under trees.
|
||||||
|
vec3 H = normalize(L + V);
|
||||||
|
float nh = max(dot(n, H), 0.0);
|
||||||
|
float specVis = mix(0.15, 1.0, sunVis);
|
||||||
|
float spec = pow(nh, 1200.0) * 120.0 + pow(nh, 160.0) * 4.0 + pow(nh, 24.0) * 0.35;
|
||||||
|
col += sunRad * spec * uSpecular * 0.05 * specVis;
|
||||||
|
|
||||||
|
// Glitter: a field of twinkling point highlights on wave facets, two scales.
|
||||||
|
// Each is a few pixels wide and very bright, so bloom and depth of field
|
||||||
|
// turn them into the soft bokeh sparkles typical of HD-2D water.
|
||||||
|
float lobe = 0.35 + pow(nh, 8.0) * 3.0;
|
||||||
|
float g = glitter(wp.xz * 3.5, t, 0.13) + glitter(wp.xz * 1.6 + 17.0, t * 0.8, 0.085) * 1.8;
|
||||||
|
g *= smoothstep(0.3, 0.7, surf.w) * (0.5 + 4.0 * F);
|
||||||
|
col += sunRad * g * lobe * uGlitter * 8.0 * specVis * max(L.y, 0.25);
|
||||||
|
|
||||||
|
// Shoreline foam: broken bands drifting along the distance-to-shore field.
|
||||||
|
float fn = vnoise(wp.xz * 4.0 + vec2(t * 0.35, -t * 0.25)) * 0.6 + vnoise(wp.xz * 9.0 - t * 0.5) * 0.4;
|
||||||
|
float d = shoreDist + (fn - 0.5) * 0.12;
|
||||||
|
float edge = 1.0 - smoothstep(0.0, uFoamWidth * 0.3, d); // solid contact line
|
||||||
|
float bands = sin(d / max(uFoamWidth, 1e-3) * 9.0 - t * 1.8) * 0.5 + 0.5; // receding wash
|
||||||
|
float wash = (1.0 - smoothstep(0.0, uFoamWidth, d)) * smoothstep(0.55, 0.85, bands * 0.6 + fn * 0.5);
|
||||||
|
float foam = clamp(max(edge, wash) * uFoamIntensity, 0.0, 1.0);
|
||||||
|
vec3 foamCol = vec3(0.92, 0.96, 1.0) * (sunRad * max(L.y, 0.0) * mix(0.4, 1.0, sunVis) + hemiAmbient(vec3(0, 1, 0))) * 0.55;
|
||||||
|
col = mix(col, foamCol, foam);
|
||||||
|
|
||||||
|
fragColor = vec4(col, 1.0);
|
||||||
|
}
|
||||||
16
assets/shaders/water.vert
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#version 450 core
|
||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 aPos;
|
||||||
|
uniform float uWaveStrength;
|
||||||
|
|
||||||
|
out vec3 vWorld;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 p = aPos;
|
||||||
|
// Gentle swell; fine detail comes from per-pixel normals.
|
||||||
|
p.y += (sin(p.x * 1.3 + uTime * 1.1) * cos(p.z * 1.1 + uTime * 0.9) +
|
||||||
|
sin(p.x * 0.7 - p.z * 0.9 + uTime * 0.6) * 0.5) * 0.018 * uWaveStrength;
|
||||||
|
vWorld = p;
|
||||||
|
gl_Position = uViewProj * vec4(p, 1.0);
|
||||||
|
}
|
||||||
BIN
assets/sprites/test_sprite.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
assets/textures/flowers.png
Normal file
|
After Width: | Height: | Size: 951 B |
BIN
assets/textures/grass_tuft.png
Normal file
|
After Width: | Height: | Size: 660 B |
BIN
assets/textures/ground_extra.png
Normal file
|
After Width: | Height: | Size: 614 B |
BIN
assets/textures/ground_extra_0.png
Normal file
|
After Width: | Height: | Size: 614 B |
BIN
assets/textures/ground_extra_1.png
Normal file
|
After Width: | Height: | Size: 575 B |
BIN
assets/textures/ground_extra_10.png
Normal file
|
After Width: | Height: | Size: 1005 B |
BIN
assets/textures/ground_extra_11.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/ground_extra_12.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/textures/ground_extra_13.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/textures/ground_extra_14.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/textures/ground_extra_15.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/textures/ground_extra_2.png
Normal file
|
After Width: | Height: | Size: 736 B |
BIN
assets/textures/ground_extra_3.png
Normal file
|
After Width: | Height: | Size: 991 B |
BIN
assets/textures/ground_extra_4.png
Normal file
|
After Width: | Height: | Size: 987 B |
BIN
assets/textures/ground_extra_5.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/ground_extra_6.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/ground_extra_7.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/ground_extra_8.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/ground_extra_9.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/ground_grass.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/textures/ground_grass_0.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/textures/ground_grass_1.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/ground_grass_10.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/ground_grass_11.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/textures/ground_grass_12.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/ground_grass_13.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/ground_grass_14.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/ground_grass_15.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/ground_grass_2.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/textures/ground_grass_3.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/textures/ground_grass_4.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/textures/ground_grass_5.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/ground_grass_6.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/textures/ground_grass_7.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/textures/ground_grass_8.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/ground_grass_9.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/ground_misc.png
Normal file
|
After Width: | Height: | Size: 512 B |
BIN
assets/textures/ground_misc_0.png
Normal file
|
After Width: | Height: | Size: 512 B |
BIN
assets/textures/ground_misc_1.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
assets/textures/ground_misc_10.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/textures/ground_misc_11.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/ground_misc_12.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/ground_misc_13.png
Normal file
|
After Width: | Height: | Size: 1007 B |
BIN
assets/textures/ground_misc_14.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/ground_misc_15.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/ground_misc_2.png
Normal file
|
After Width: | Height: | Size: 489 B |
BIN
assets/textures/ground_misc_3.png
Normal file
|
After Width: | Height: | Size: 418 B |
BIN
assets/textures/ground_misc_4.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/textures/ground_misc_5.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/ground_misc_6.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/ground_misc_7.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/ground_misc_8.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |