Compare commits

...

89 commits

Author SHA1 Message Date
Kelsi
06979e5c5c Prevent player snapping to WMO roofs when jumping inside buildings
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Anchor WMO floor probe to last ground Z when airborne so the detection
ceiling doesn't rise with the jump and catch roof/ceiling geometry.
2026-02-23 11:03:18 -08:00
Kelsi
bf997e1900 Distance-cull terrain derivative normal mapping
Fade bump strength from full at 50 units to zero at 125 units to
avoid noisy/harsh appearance on distant terrain.
2026-02-23 10:54:56 -08:00
Kelsi
9a1b78bffd Fix character preview facing and add 4x MSAA to preview render target
Character was facing stage-left (yaw 180) instead of toward camera;
corrected default yaw to 90. Added MSAA support to VkRenderTarget with
automatic resolve attachment, and enabled 4x MSAA for the character
preview off-screen pass.
2026-02-23 10:48:26 -08:00
Kelsi
d65b170774 Make shadows follow player movement continuously
Remove freeze-while-moving and idle smoothing logic from shadow
center computation. Texel snapping already prevents shimmer, so
the shadow projection can track the player directly each frame.
2026-02-23 08:47:38 -08:00
Kelsi
5072c536b5 Fix sun quad square artifact with hard radial discard cutoff
Add hard discard at radius 0.35 so no fragment beyond that point
reaches the additive blend stage. Tighten disc, glow, and edge
fade to fit within the cutoff boundary.
2026-02-23 08:43:50 -08:00
Kelsi
0a1e240831 Fix WMO shadow receiving and enable shadows by default
Remove isInterior restriction from WMO shadow sampling so city
buildings (flagged as interior groups) correctly receive shadows.
Apply shadow to interior-lit surfaces. Enable shadows by default
and persist the setting across sessions.
2026-02-23 08:40:23 -08:00
Kelsi Rae Davis
c3ed915649
Merge pull request #4 from Kittnz/vulkan_win
Make this compatible to build on MSVS 2022
2026-02-23 08:38:37 -08:00
Kelsi
52ef199af3 Reduce default max zoom out from 33 to 22 units 2026-02-23 08:11:49 -08:00
Kelsi
b70f08d14f Add proportional zoom and reduced default max zoom with extended toggle
Zoom speed now scales with distance for fine control near the character.
Default max zoom out reduced from 50 to 33 units. New "Extended Camera
Zoom" toggle in Gameplay settings restores the full 50-unit range.
2026-02-23 08:09:27 -08:00
Kelsi
5cfb0817ed Fix sun quad visibility, minimap opacity, audio scaling, and rename music toggle
- Tighten celestial glow falloff and edge fade to eliminate visible quad boundary
- Add opacity support to minimap display shader, synced with UI opacity setting
- Fix audio double/triple-scaling by removing redundant master volume multiplications
- Rename "Original Soundtrack" toggle to "Enable WoWee Music"
- Add About tab with developer info and GitHub link
2026-02-23 08:01:20 -08:00
Kelsi
a250c20d84 Fix audio volume double/triple-scaling and add About tab
Audio was being scaled by master volume multiple times: once via
ma_engine_set_volume, again per-sound in AudioEngine, and again
via pre-multiplication in the UI. Removed redundant master volume
multiplications so each channel volume is independent and master
applies once through the engine. Added About tab to settings with
developer info and GitHub link.
2026-02-23 07:51:10 -08:00
Kelsi
83d7f26f33 Fix visible square behind sun by switching celestial to additive blending
Alpha blending caused faint quad edges to be visible against the sky
gradient. Additive blending correctly adds glow light without the
outline artifact. Edge fade starts earlier and discard threshold raised.
2026-02-23 07:40:51 -08:00
Kelsi
59f487c941 Fix foot splash particles being cleared immediately when wading
Ripples were cleared every frame the player wasn't swimming, which
destroyed foot splash particles the same frame they were spawned.
Now particles expire naturally via their lifetime.
2026-02-23 07:37:01 -08:00
Kelsi
a7102ab742 Fix foliage DXT black fringe, insect depth occlusion, and ambient creature animation
- Replace dark edge RGB on alpha-tested foliage with mip-4 average color
  proportional to alpha (low alpha = low trust in original RGB)
- Raise foliage alpha cutoff to 0.4 and remove dither band for cleaner edges
- Disable depth test on insect particles so they don't vanish behind foliage
- Exempt dragonflies/butterflies/moths from foliage animation freeze
2026-02-23 07:34:29 -08:00
kittnz
9dd15ef922 Make this compatible to build with MSVS 2022 2026-02-23 16:30:49 +01:00
Kelsi
01c08b93b0 Exempt ambient creatures from foliage animation freeze
Fireflies, dragonflies, butterflies, and moths no longer get
disableAnimation/foliage classification so their bone animation
and particles run normally.
2026-02-23 07:21:45 -08:00
Kelsi
4db97e37b7 Add ambient insect particles near water vegetation, fix firefly particles, and improve water foam
- Spawn dark point-sprite insects buzzing around cattails/reeds/kelp/seaweed
- Fix firefly M2 particles: exempt from alpha dampening and forced gravity
- Make water shoreline/crest foam more irregular with UV warping and bluer tint
2026-02-23 07:18:44 -08:00
Kelsi
c35b40391f Add water footstep splashes and reduce lake wave amplitude
- Play WaterFootstep splash sounds when wading in shallow water
- Spawn foot splash particles at water surface on each water footstep
- Reduce inland water wave amplitude from 0.18 to 0.08 for calmer lakes
2026-02-23 06:58:46 -08:00
Kelsi
93f873b521 Allow texture load retries instead of permanently caching failures
Remove negative cache for transient load failures in M2, terrain, and
character renderers. Failed textures return white and will be retried
on next model/tile load when assets may have finished streaming.
2026-02-23 06:51:06 -08:00
Kelsi
3e6de8485b Fix ground clutter height sampling and terrain shader GPU crash
- Fix HeightMap::getHeight() to use interleaved 17-wide row layout
  matching MCVT storage (was using wrong 9-wide contiguous indexing)
- Guard terrain bump mapping normalize against zero-length vectors
  to prevent NaN propagation and GPU faults
2026-02-23 06:46:31 -08:00
Kelsi
1e08d6ff22 Show target buffs/debuffs on target frame and fix stance error message
- Add aura icons below target frame with buff/debuff color coding
- Show spell icons with hover tooltips and duration countdowns
- Change SPELL_FAILED_ONLY_SHAPESHIFT message to cover warrior stances
2026-02-23 06:37:15 -08:00
Kelsi
b7da2408c8 Add derivative-based normal mapping to terrain for per-pixel detail 2026-02-23 06:31:54 -08:00
Kelsi
f2f6ffd2cd Fix voice gender using server data and update loading screen UI
- Use authoritative playerRace/playerGender at spawn for voice profiles
  instead of unreliable model name parsing
- Support nonbinary gender with useFemaleModel body type fallback
- Move voice setup into spawnPlayerCharacter() for all spawn paths
- Remove legacy single-player default Human Male clip preloading
- Make loading screen text black and move progress bar to top
2026-02-23 06:22:30 -08:00
Kelsi
efc7e65dfc Optimize M2/WMO render loop: cache UBO pointers, precompute model flags, reduce rebinds
- Cache material UBO mapped pointers at creation time, eliminating
  per-batch vmaGetAllocationInfo() calls in the hot render path
- Precompute foliage/elven/lantern/kobold model name classifications
  at load time instead of per-instance string operations every frame
- Remove redundant descriptor set and push constant rebinds on WMO
  pipeline switches (preserved across compatible layouts)
- Pre-allocate glow sprite descriptor set once at init instead of
  allocating from the pool every frame
2026-02-23 06:06:24 -08:00
Kelsi
77012adbc6 Add alpha-tested foliage shadows: per-batch texture binding and shadow map receiving
Shadow casting: foliage batches now bind their actual texture in the shadow
pass with alpha testing, producing leaf-shaped shadows instead of solid cards.
Uses a per-frame resettable descriptor pool for texture sets.

Shadow receiving: foliage fragments now sample the shadow map with PCF
instead of using a flat constant darkening.
2026-02-23 05:55:03 -08:00
Kelsi
6fc2c36dae Fix shadows not rendering until player moves by deferring shadow pass until character position is set 2026-02-23 05:45:22 -08:00
Kelsi
639df4815e Handle SMSG_GAMEOBJECT_CUSTOM_ANIM to unfreeze gameobjects on use
When the server sends a custom animation packet (e.g. chest being
opened), unfreeze the M2 instance so it plays its open animation.
2026-02-23 05:39:02 -08:00
Kelsi
58681753e5 Freeze gameobject M2 animations to prevent cycling
Gameobject M2 instances (books, crates, chests) were continuously
cycling their animations because M2Renderer unconditionally loops
all sequences. Added setInstanceAnimationFrozen() and freeze all
gameobject instances at creation time so they stay in their bind pose.
2026-02-23 05:31:02 -08:00
Kelsi
a58115041f Fix spline parsing always falling through to compact format
The legacy spline parser successfully read points but then unconditionally
rewound and re-parsed as compact format. When the data was actually legacy
format, the compact parser would read garbage and fail, causing the entire
update block (and all subsequent blocks in the packet) to be dropped.
This made creatures invisible when their spawn packet contained a spline.
2026-02-23 05:13:26 -08:00
Kelsi
e98450f283 Upgrade displayId=0 creature spawn skip to WARNING level
Makes it visible in logs when a creature entity spawns without a
display ID, which would cause it to be invisible but still active.
2026-02-23 05:11:35 -08:00
kittnz
590131590c Make this compatible to build on window 2026-02-23 14:09:51 +01:00
Kelsi
30e9998a86 Add diagnostics for invisible creatures and update shadow signatures
- Log warning when WotLK M2 skin file is missing (causes invisible creatures)
- Move skin loading inside version >= 264 check to skip unnecessary readFile
- Update renderShadow header signatures to match implementation (shadow culling)
2026-02-23 04:59:39 -08:00
Kelsi
7dc9bf3766 Fix M2 bone buffer leak on instance removal
removeInstance() and removeInstances() were erasing M2Instances without
calling destroyInstanceBones(), leaking VMA bone buffers permanently.
This caused framerate to drop and never recover after NPC encounters.
2026-02-23 04:52:40 -08:00
Kelsi
2124761ea8 Add distance culling to shadow passes for CPU-bound shadow perf
All three shadow renderers (WMO, M2, Character) were iterating every
loaded instance with zero culling. Now skip instances outside the
180-unit shadow frustum radius via squared-distance check.
2026-02-23 04:48:26 -08:00
Kelsi
2cfa9d6b19 Remove per-frame chrono profiling from application and game handler
Strip ~30 chrono::now() calls per frame from Application::update() and
GameHandler::update() that existed only for periodic LOG_DEBUG dumps.
Retain renderer timing used by the live performance HUD overlay.
2026-02-23 04:41:22 -08:00
Kelsi
9e1a913060 Increase texture cache budgets to 4GB and cap repetitive warnings
Raise all texture cache defaults from 1GB to 4GB to reduce rejections.
Cap cache-full warnings (texture + model) to 3 messages per renderer,
and cap update block parse errors to 5 messages.
2026-02-23 04:32:58 -08:00
Kelsi
820a36ac12 Remove per-frame profiling instrumentation and periodic debug logging
Strip 26 chrono::now() timing calls per frame from renderer update loop,
periodic LOG_INFO/LOG_DEBUG from terrain/character/quest/heartbeat paths,
and dead m2ProfileCounter variable.
2026-02-23 04:26:20 -08:00
Kelsi
e8c2344226 Remove leftover debug logging from render hot paths
Strip per-frame/periodic logging from CharacterRenderer (batch dump,
instance count) and M2Renderer (frame timing profiler) to avoid
unnecessary string formatting and I/O in render loops.
2026-02-23 04:18:35 -08:00
Kelsi
98fb6b47da Add 9-tap PCF soft shadows and normal-offset bias to all fragment shaders
Replaces single-tap shadow sampling with 3x3 grid PCF (36-sample equivalent
with hardware bilinear filtering) for smooth shadow edges. Adds normal-offset
bias to eliminate shadow acne on oblique surfaces without peter-panning.
2026-02-23 04:14:27 -08:00
Kelsi
ae3903561c Fix specular direction by correcting front face winding for Vulkan Y-flip
The projection matrix Y-flip (projectionMatrix[1][1] *= -1) reverses
triangle winding from the GPU's perspective. With the default
VK_FRONT_FACE_COUNTER_CLOCKWISE, gl_FrontFacing was inverted, causing
all fragment shaders (M2, WMO, character) to flip normals on front
faces instead of back faces, putting specular highlights on the wrong
side of surfaces.
2026-02-23 04:02:21 -08:00
Kelsi
ef1e5abe8e Add shader-driven tree beautification: wind sway, SSS, color variation, AO
- Vertex wind animation: 3-layer displacement (trunk/branch/leaf) with
  quadratic height scaling so bases stay grounded
- Shadow pass: matching vertex displacement split into foliage/non-foliage
  passes, removed UV-wiggle approach
- Leaf subsurface scattering: warm backlit glow when looking toward sun
- Per-instance color variation: hue/brightness from position hash via flat
  varying to avoid interpolation flicker
- Canopy ambient occlusion: height-based darkening of tree interiors
- Detail normal perturbation: UV-only procedural normals to break flat cards
- Bayer 4x4 ordered dither replacing sin-hash noise for alpha edges
- Foliage skips shadow map sampling and specular to prevent flicker from
  swaying geometry sampling unstable shadow/highlight values
2026-02-23 03:53:50 -08:00
Kelsi
4511de8d38 Fix lamp posts rendering as glass by using texture name for window detection
Instead of relying on material flag 0x40 (F_CLAMP_S, not F_WINDOW) to
identify glass materials, detect windows by checking for "window" in the
texture path. This correctly applies glass to window textures while
leaving lamp posts and other geometry opaque.
2026-02-23 03:29:07 -08:00
Kelsi
a7cf0d0c4e Fix WMO LOD shell culling and MOGP header parsing
- Fix MOGP header: skip 8-byte groupName/descriptiveName prefix before flags
- Fix fogIndices: read as 4×uint8 (4 bytes) instead of 4×uint32 (16 bytes)
- Detect LOD shell groups: city shells, facades, flag 0x80 indoor, low-vert
- Per-group distance culling at 196 units instead of whole-WMO distance
2026-02-23 03:23:18 -08:00
Kelsi
3ffb7ccc50 Fix lamp posts as glass and hide distance-only LOD groups when close
Two WMO rendering fixes:

1. Glass batch merging: BatchKey didn't include isWindow, so window and
   non-window batches sharing the same texture got merged together. If
   the window batch was processed first, the entire merged batch (lamp
   base, iron frame, etc.) rendered as transparent glass. Add isWindow
   to the batch key so glass/non-glass batches stay separate.

2. LOD group culling: WMO groups named with "LOD" are distance-only
   impostor geometry (e.g. cathedral tower extension, hill tower). They
   should only render beyond 200 units. Store raw MOGN chunk for
   offset-based name lookup, detect "lod" in group names during load,
   and skip LOD groups in both main and shadow passes when camera is
   within range.
2026-02-23 01:54:05 -08:00
Kelsi
4acba4110f Fix lamp posts rendering as glass by narrowing window material check
F_SIDN (0x20) is the night-glow/self-illuminated flag, not a window
flag. Only F_WINDOW (0x40) should trigger transparent glass rendering.
The previous mask (0x60) caught both flags, making lamp post bases,
iron frames, and other SIDN-flagged surfaces render as transparent
glass with Fresnel reflections instead of opaque materials.
2026-02-23 01:47:18 -08:00
Kelsi
bb67bfb9a3 Increase normal map Sobel strength and POM scale for character models
Hand-painted weapon/armor textures have baked-in lighting that produces
low-contrast gradients in the Sobel filter. Bump strength from 2.0 to
5.0 to extract visible surface detail. Also increase POM scale from
0.03 to 0.06 since character models are small with dense UVs where
the WMO-tuned 0.03 was barely perceptible.
2026-02-23 01:43:09 -08:00
Kelsi
9eeb9ce64d Add normal mapping and parallax occlusion mapping for character models
Extends the WMO normal mapping/POM system to character M2 models with
bone-skinned tangents. Auto-generates normal/height maps from diffuse
textures using luminance→height, Sobel→normals (same algorithm as WMO).

- Expand vertex buffer from M2Vertex (48B) to CharVertexGPU (56B) with
  tangent vec4 computed via Lengyel's method in setupModelBuffers()
- Tangents are bone-transformed and Gram-Schmidt orthogonalized in the
  vertex shader, output as TBN for fragment shader consumption
- Fragment shader gains POM ray marching, normal map blending, and LOD
  crossfade via dFdx/dFdy (identical to WMO shader)
- Descriptor set 1 extended with binding 2 for normal/height sampler
- Settings (enable, strength, POM quality) wired from game_screen.cpp
  to both WMO and character renderers via shared UI controls
2026-02-23 01:40:23 -08:00
Kelsi
3c31c43ca6 Enable parallax mapping by default 2026-02-23 01:23:24 -08:00
Kelsi
b12bc1f71e Default normal map strength to 0.8 2026-02-23 01:21:58 -08:00
Kelsi
bec3190b08 Fix POM distortions and add normal map strength slider
POM fixes: use blurred height only for ray march (keep crisp Sobel for
normals), reduce pomScale 0.03→0.012, clamp grazing angle denominator
to 0.15, hard-limit max UV offset, smooth fadeout at steep view angles.

Add Normal Map Strength slider (0.0-2.0) in Video settings for user
control over surface detail intensity. Persisted across sessions.
2026-02-23 01:18:42 -08:00
Kelsi
eaceb58e77 Add normal mapping and parallax occlusion mapping for WMO surfaces
Generate normal+height maps from diffuse textures at load time using
luminance-to-height and Sobel 3x3 filtering. Compute per-vertex tangents
via Lengyel's method for TBN basis construction.

Fragment shader uses screen-space UV derivatives (dFdx/dFdy) for smooth
LOD crossfade and angle-adaptive POM sample counts. Flat textures
naturally produce low height variance, causing POM to self-select off.

Settings: Normal Mapping on by default, POM off by default with
Low/Medium/High quality presets. Persisted across sessions.
2026-02-23 01:10:58 -08:00
Kelsi
1b16bcf71f Add glass pipeline for WMO windows with Fresnel-based transparency
Dedicated Vulkan pipeline with alpha blending AND depth writes so
windows look transparent at oblique angles without see-through artifacts.
Fresnel alpha ranges from 0.4 (grazing) to 0.95 (head-on) with sun
glint, reflections, and silver-lining specular.
2026-02-23 00:43:14 -08:00
Kelsi
fb4ff46fe3 Fix WMO water rendering: correct MLIQ parsing, tile masking, and depth effects
- Fix MLIQ vertex stride: each vertex is 8 bytes (4 flow + 4 height), not 4
- Use MLIQ tile flags to mask out tiles with no liquid (bridges, covered areas)
- Disable wave displacement on WMO water to prevent edge slosh artifacts
- Convert screen-space depth to vertical depth for shoreline foam and water
  transparency, preventing false shoreline effects on occluding geometry
- Add underwater blue fog overlay and scene fog shift (terrain water only)
- Add getNearestWaterHeightAt to avoid false underwater detection from
  elevated WMO water surfaces
- Tint refracted scene toward water color to mask occlusion edge artifacts
- Lower WMO water by 1 unit to match terrain water level
2026-02-23 00:18:32 -08:00
Kelsi
6563eebb60 Enhanced sky atmosphere with DBC-driven colors, sun lighting, and zone weather
- Skybox now uses DBC sky colors (skyTop/skyMiddle/skyBand1/skyBand2) instead
  of hardcoded C++ color curves, with 3-band gradient and Rayleigh/Mie scattering
- Clouds receive sun direction for lit edges, self-shadowing, and silver lining
- Fixed sun quad box artifact with proper edge fade in celestial shader
- Lens flare attenuated by fog, cloud density, and weather intensity
- Replaced garish green/purple lens flare ghosts with warm natural palette
- Added zone-based weather system for single-player mode with per-zone rain/snow
  configuration, probability-based activation, and smooth intensity transitions
- Server SMSG_WEATHER remains authoritative when connected to a server
2026-02-22 23:20:13 -08:00
Kelsi
085fd09b9d Fix water transparency on resize by rebuilding scene history resources
Water scene history textures and 1x pass resources were not recreated on
window resize/fullscreen, causing stale undersized textures that produced
directional transparency artifacts.
2026-02-22 22:42:58 -08:00
Kelsi
03a62526e1 Add player water ripples and separate 1x water pass for MSAA compatibility
Player interaction ripples: vertex shader adds radial damped-sine displacement
centered on player position, fragment shader adds matching normal perturbation
for specular highlights. Player XY packed into shadowParams.zw, ripple strength
into fogParams.w. Separate 1x render pass for water when MSAA is active to
avoid MSAA-induced darkening — water renders after main pass resolves, using
the resolved swapchain image and depth resolve target. Water 1x framebuffers
rebuilt on swapchain recreate (window resize).
2026-02-22 22:34:48 -08:00
Kelsi
67e63653a4 Stabilize Vulkan shadow pipeline diagnostics and compatibility path
- Fix shadow depth image layout transitions by tracking per-frame old/new layouts.
- Update receiver shadow projection to Vulkan clip-depth convention.
- Test inverted shadow compare op path (GREATER_OR_EQUAL).
- Switch shadow compare samplers to NEAREST filtering for broader Vulkan compatibility.
- Expand shadow caster coverage by disabling caster cull filtering in WMO/M2/Character shadow pipelines.
- Keep light-space matrix path on stable character-centered framing.
2026-02-22 10:25:33 -08:00
Kelsi
2c5e0dd313 Fix Vulkan shadow light direction and restore ground-clutter cutout visibility 2026-02-22 09:47:39 -08:00
Kelsi
bd0305f6dd Stabilize Vulkan rendering state for minimap, foliage, and water 2026-02-22 09:34:27 -08:00
Kelsi
8efc1548dc Fix minimap arrow orientation and ground-detail foliage transparency 2026-02-22 08:44:16 -08:00
Kelsi
0631b9f5dc Reduce update-object and inventory update overhead 2026-02-22 08:37:02 -08:00
Kelsi
37888c666d Optimize update-object field mask parsing 2026-02-22 08:30:18 -08:00
Kelsi
3a9bd0d4e5 Fix update-object spline parsing regression 2026-02-22 08:27:17 -08:00
Kelsi
17a2a1f7ef Optimize world socket buffer handling and logging 2026-02-22 08:16:54 -08:00
Kelsi
9c8cd44803 Optimize threading and texture fallback stability 2026-02-22 08:12:08 -08:00
Kelsi
f4d947fab1 Make frame timing logs opt-in 2026-02-22 07:45:49 -08:00
Kelsi
6d55c19987 Stabilize net parsing and reduce texture-cache churn 2026-02-22 07:44:32 -08:00
Kelsi
ae88b226b5 Stabilize streaming memory and parser handling; revert socket recv optimizations 2026-02-22 07:26:54 -08:00
Kelsi
c914295d20 Reduce logging overhead and reuse WMO culling futures 2026-02-22 06:42:15 -08:00
Kelsi
4ea4cb761c Optimize logging and make world packet parser callback-safe 2026-02-22 06:38:41 -08:00
Kelsi
85c8b5d5f4 Optimize logging overhead and character animation threading 2026-02-22 06:32:49 -08:00
Kelsi
9d647e5622 Fix Vulkan shadow shader descriptor set mismatch 2026-02-22 06:28:18 -08:00
Kelsi
57b049fb2a Fix character disappearance from transient Vulkan resource lifetime 2026-02-22 06:21:18 -08:00
Kelsi
7dd1dada5f Work on character rendering and frustrum culling etc 2026-02-22 05:58:45 -08:00
Kelsi
fc5294eb0f Update readme and build instructions docs 2026-02-22 05:09:16 -08:00
Kelsi
e8e859384e Fix minimap MSAA pipeline, defer swapchain recreation, fix player character spawn order
- Add .setMultisample() to minimap display pipeline and recreatePipelines() for MSAA changes
- Defer all swapchain recreation in window.cpp to beginFrame() via markSwapchainDirty()
  to prevent mid-frame render pass destruction crashes on resolution/fullscreen change
- Move spawnPlayerCharacter() call to after loadTestTerrain() where character renderer exists
2026-02-22 03:49:44 -08:00
Kelsi
ebd0084c22 Fix MSAA crash by deferring change to between frames, fix M2 GO orientation
MSAA change was called mid-frame from settings UI, destroying the render pass
and framebuffers while the command buffer was still recording. Now deferred
via pendingMsaaSamples_ flag, applied in beginFrame() before any GPU state.

Also add +180° to M2 game object orientation to fix facing direction.
2026-02-22 03:37:47 -08:00
Kelsi
325254dfcb Port UI icon textures from OpenGL to Vulkan, fix loading screen clear values
Replace all glGenTextures/glTexImage2D calls in UI code with Vulkan texture
uploads via new VkContext::uploadImGuiTexture() helper. This fixes segfaults
from calling OpenGL functions without a GL context (null GLEW function pointers).

- Add uploadImGuiTexture() to VkContext with staging buffer upload pattern
- Convert game_screen, inventory_screen, spellbook_screen, talent_screen
  from GLuint/GL calls to VkDescriptorSet/Vulkan uploads
- Fix loading_screen clearValueCount (was 1, needs 2 or 3 for MSAA)
2026-02-22 03:32:08 -08:00
Kelsi
b1a9d231c7 Fix MSAA 8x crash: clearValueCount must match attachment count
Render pass begin used 2 clear values but MSAA render pass has 3
attachments (MSAA color, depth, resolve). Vulkan requires clear
value count >= attachment count, causing a driver crash at 8x.

Also fix renderYawM2 reference removed in previous commit.
2026-02-22 03:11:21 -08:00
Kelsi
786b35ae0d Remove +90° rotation from M2 game object orientation
M2 game objects don't need the extra 90° yaw offset that was
previously applied — orientation from server is used directly.
2026-02-22 03:07:33 -08:00
Kelsi
fa1867cf2f Fix MSAA 8x crash and eliminate redundant GPU stalls
- Add error handling: revert to 1x if recreateSwapchain fails
- Clamp requested MSAA to device maximum before applying
- Retry MSAA color image allocation without TRANSIENT on failure
- Remove redundant vkDeviceWaitIdle from WMO/M2/Character recreatePipelines
  (caller already waits once, was causing ~13 stalls instead of 1)
2026-02-22 03:05:55 -08:00
Kelsi
e12141a673 Add configurable MSAA anti-aliasing, update auth screen and terrain shader
- MSAA: conditional 2-att (off) vs 3-att (on) render pass with auto-resolve
- MSAA: multisampled color+depth images, query max supported sample count
- MSAA: .setMultisample() on all 25+ main-pass pipelines across 17 renderers
- MSAA: recreatePipelines() on every sub-renderer for runtime MSAA changes
- MSAA: Renderer::setMsaaSamples() orchestrates swapchain+pipeline+ImGui rebuild
- MSAA: Anti-Aliasing combo (Off/2x/4x/8x) in Video settings, persisted
- Update auth screen assets and terrain fragment shader
2026-02-22 02:59:24 -08:00
Kelsi
6d213ad49b Fix M2 game object orientation to use +90° render yaw like characters
M2 game objects (signs, posts) were using orientation - 90° while
characters/NPCs use orientation + 90° for the canonical-to-render yaw
conversion. Both renderers build model matrices identically, so they
need the same offset. The old -90° was calibrated when terrain was
rotated 90°; with correct terrain the signs appeared sideways.
2026-02-22 02:31:16 -08:00
Kelsi
25bd05a631 Add Discord badge to README 2026-02-21 22:09:52 -08:00
Kelsi
b012906887 Add Vulkan Y-flip to projection matrix and ignore node_modules
Negate projection[1][1] to correct for Vulkan's Y-down NDC convention.
Also add node_modules/ to .gitignore to prevent accidental commits.
2026-02-21 22:05:11 -08:00
Kelsi
4fc3689dcc Fix sky and clouds orientation for Z-up world coordinates
Skybox: replace sphere-mesh approach with a fullscreen triangle that
reconstructs the world-space ray direction analytically via inverse
projection/view matrices. Eliminates clip.w=0 degeneracy at the horizon
and correctly maps altitude to dir.z in the Z-up coordinate system.

Clouds: hemisphere mesh was storing altitude in aPos.y (Y-up convention);
the Z-up view matrix projected this sideways, making clouds appear
vertical. Store altitude in aPos.z and update the fragment shader to
read dir.z as altitude and dir.xy as the horizontal UV plane.
2026-02-21 22:04:17 -08:00
Kelsi
69cf39ba02 Activate WMO/char/M2 render loop, purge dead GL block, add underwater overlay
- renderWorld() now calls wmoRenderer, characterRenderer, m2Renderer (+smoke,
  particles) in the correct opaque→transparent order; water moved after all
  opaques; per-subsystem timing active in live path
- Deleted the 310-line #if 0 GL stub block and removed #include <GL/glew.h>
  (last GL reference in renderer.cpp)
- Full-screen overlay pipeline (postprocess.vert + overlay.frag, alpha blend,
  no depth test/write) for underwater tint; lazily initialized, renders a blue
  tint when camera is meaningfully below the water surface; canal vs open-water
  tint colours preserved from original design
- overlay.frag.glsl / overlay.frag.spv added
2026-02-21 22:04:17 -08:00
Kelsi
dea52744a4 Character renderer is fully Vulkan. 2026-02-21 22:04:17 -08:00
Kelsi
83b576e8d9 Vulcan Nightmare
Experimentally bringing up vulcan support
2026-02-21 22:04:17 -08:00
242 changed files with 21421 additions and 9555 deletions

1
.gitignore vendored
View file

@ -89,3 +89,4 @@ ingest/
# Local texture dumps / extracted art should never be committed
assets/textures/
node_modules/

View file

@ -1,146 +1,88 @@
# Build Instructions
# WoWee Build Instructions
This project builds as a native C++ client for WoW 3.3.5a in online mode.
This document provides platform-specific build instructions for WoWee.
## 1. Install Dependencies
---
### Ubuntu / Debian
## 🐧 Linux (Ubuntu / Debian)
### Install Dependencies
```bash
sudo apt update
sudo apt install -y \
cmake build-essential pkg-config git \
libsdl2-dev libglew-dev libglm-dev \
libssl-dev zlib1g-dev \
libavformat-dev libavcodec-dev libswscale-dev libavutil-dev \
libstorm-dev
sudo apt install -y build-essential cmake pkg-config git libsdl2-dev libglew-dev libglm-dev libssl-dev zlib1g-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libunicorn-dev libstorm-dev
```
If `libstorm-dev` is unavailable in your distro repos, build StormLib from source:
---
## 🐧 Linux (Arch)
### Install Dependencies
```bash
cd /tmp
git clone https://github.com/ladislav-zezula/StormLib.git
cd StormLib
mkdir build && cd build
cmake ..
make -j"$(nproc)"
sudo make install
sudo ldconfig
sudo pacman -S --needed base-devel cmake pkgconf git sdl2 glew glm openssl zlib ffmpeg unicorn stormlib
```
### Fedora
---
## 🐧 Linux (All Distros)
### Clone Repository
Always clone with submodules:
```bash
sudo dnf install -y \
cmake gcc-c++ make pkg-config git \
SDL2-devel glew-devel glm-devel \
openssl-devel zlib-devel \
ffmpeg-devel \
StormLib-devel
git clone --recurse-submodules https://github.com/Kelsidavis/WoWee.git
cd WoWee
```
### Arch
If you already cloned without submodules:
```bash
sudo pacman -S --needed \
cmake base-devel pkgconf git \
sdl2 glew glm openssl zlib ffmpeg stormlib
git submodule update --init --recursive
```
## 2. Clone + Prepare
```bash
git clone https://github.com/Kelsidavis/WoWee.git
cd wowee
git clone https://github.com/ocornut/imgui.git extern/imgui
```
## 3. Configure + Build
### Build
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
```
Binary output:
---
```text
build/bin/wowee
## 🪟 Windows (Visual Studio 2022)
### Install
- Visual Studio 2022
- Desktop development with C++
- CMake tools for Windows
### Clone
```powershell
git clone --recurse-submodules https://github.com/Kelsidavis/WoWee.git
cd WoWee
```
## 4. Provide WoW Data (Extract + Manifest)
### Build
Wowee loads assets from an extracted loose-file tree indexed by `manifest.json` (it does not read MPQs at runtime).
Open the folder in Visual Studio (it will detect CMake automatically)
or build from Developer PowerShell:
### Option A: Extract into `./Data/` (recommended)
Run:
```bash
# WotLK 3.3.5a example
./extract_assets.sh /path/to/WoW/Data wotlk
```
The output includes:
```text
Data/
manifest.json
interface/
sound/
world/
expansions/
```
### Option B: Use an existing extracted data tree
Point wowee at your extracted `Data/` directory:
```bash
export WOW_DATA_PATH=/path/to/extracted/Data
```
## 5. Run
```bash
./build/bin/wowee
```
## 6. Local AzerothCore (Optional)
If you are using a local AzerothCore Docker stack, start it first and then connect from the client realm screen.
See:
- `docs/server-setup.md`
## Troubleshooting
### `StormLib` not found
Install distro package or build from source (section 1).
### `ImGui` missing
Ensure `extern/imgui` exists:
```bash
git clone https://github.com/ocornut/imgui.git extern/imgui
```
### Data not found at runtime
Verify `Data/manifest.json` exists (or re-run `./extract_assets.sh ...`), or set:
```bash
export WOW_DATA_PATH=/path/to/extracted/Data
```
### Clean rebuild
```bash
rm -rf build
```powershell
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
cmake --build build --config Release
```
---
## ⚠️ Notes
- Case matters on Linux (`WoWee` not `wowee`).
- Always use `--recurse-submodules` when cloning.
- If you encounter missing headers for ImGui, run:
```bash
git submodule update --init --recursive
```

View file

@ -14,6 +14,7 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# Options
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(WOWEE_BUILD_TESTS "Build tests" OFF)
option(WOWEE_ENABLE_ASAN "Enable AddressSanitizer (Debug builds)" OFF)
# Opcode registry generation/validation
find_package(Python3 COMPONENTS Interpreter QUIET)
@ -34,8 +35,13 @@ endif()
# Find required packages
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
find_package(GLEW REQUIRED)
find_package(Vulkan REQUIRED)
# GL/GLEW kept temporarily for unconverted sub-renderers during Vulkan migration.
# These files compile against GL types but their code is never called — the Vulkan
# path is the only active rendering backend. Remove in Phase 7 when all renderers
# are converted and grep confirms zero GL references.
find_package(OpenGL QUIET)
find_package(GLEW QUIET)
find_package(OpenSSL REQUIRED)
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
@ -45,7 +51,7 @@ else()
find_package(PkgConfig REQUIRED)
endif()
if(PkgConfig_FOUND)
pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libswscale libavutil)
pkg_check_modules(FFMPEG libavformat libavcodec libswscale libavutil)
else()
# Fallback for MSVC/vcpkg — find FFmpeg libraries manually
find_path(FFMPEG_INCLUDE_DIRS libavformat/avformat.h)
@ -54,9 +60,15 @@ else()
find_library(AVUTIL_LIB NAMES avutil)
find_library(SWSCALE_LIB NAMES swscale)
set(FFMPEG_LIBRARIES ${AVFORMAT_LIB} ${AVCODEC_LIB} ${AVUTIL_LIB} ${SWSCALE_LIB})
if(NOT AVFORMAT_LIB)
message(FATAL_ERROR "FFmpeg not found. On Windows install via MSYS2: mingw-w64-x86_64-ffmpeg")
endif()
endif()
if(FFMPEG_INCLUDE_DIRS AND AVFORMAT_LIB)
set(HAVE_FFMPEG TRUE)
message(STATUS "Found FFmpeg: ${AVFORMAT_LIB}")
elseif(FFMPEG_FOUND)
set(HAVE_FFMPEG TRUE)
else()
set(HAVE_FFMPEG FALSE)
message(WARNING "FFmpeg not found — video_player will be disabled. Install via vcpkg: ffmpeg:x64-windows")
endif()
# Unicorn Engine (x86 emulator for cross-platform Warden module execution)
@ -77,13 +89,68 @@ if(NOT glm_FOUND)
message(STATUS "GLM not found, will use system includes or download")
endif()
# GLM GTX extensions (quaternion, norm, etc.) require this flag on newer GLM versions
add_compile_definitions(GLM_ENABLE_EXPERIMENTAL)
add_compile_definitions(GLM_ENABLE_EXPERIMENTAL GLM_FORCE_DEPTH_ZERO_TO_ONE)
if(WIN32)
add_compile_definitions(NOMINMAX _CRT_SECURE_NO_WARNINGS)
endif()
# SPIR-V shader compilation via glslc
find_program(GLSLC glslc HINTS ${Vulkan_GLSLC_EXECUTABLE} "$ENV{VULKAN_SDK}/bin")
if(GLSLC)
message(STATUS "Found glslc: ${GLSLC}")
else()
message(WARNING "glslc not found. Install the Vulkan SDK or vulkan-tools package.")
message(WARNING "Shaders will not be compiled to SPIR-V.")
endif()
# Function to compile GLSL shaders to SPIR-V
function(compile_shaders TARGET_NAME)
set(SHADER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders)
set(SPV_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets/shaders)
file(MAKE_DIRECTORY ${SPV_DIR})
file(GLOB GLSL_SOURCES "${SHADER_DIR}/*.glsl")
set(SPV_OUTPUTS)
foreach(GLSL_FILE ${GLSL_SOURCES})
get_filename_component(FILE_NAME ${GLSL_FILE} NAME)
# e.g. skybox.vert.glsl -> skybox.vert.spv
string(REGEX REPLACE "\\.glsl$" ".spv" SPV_NAME ${FILE_NAME})
set(SPV_FILE ${SPV_DIR}/${SPV_NAME})
# Determine shader stage from filename
if(FILE_NAME MATCHES "\\.vert\\.glsl$")
set(SHADER_STAGE vertex)
elseif(FILE_NAME MATCHES "\\.frag\\.glsl$")
set(SHADER_STAGE fragment)
elseif(FILE_NAME MATCHES "\\.comp\\.glsl$")
set(SHADER_STAGE compute)
elseif(FILE_NAME MATCHES "\\.geom\\.glsl$")
set(SHADER_STAGE geometry)
else()
message(WARNING "Cannot determine shader stage for: ${FILE_NAME}")
continue()
endif()
add_custom_command(
OUTPUT ${SPV_FILE}
COMMAND ${GLSLC} -fshader-stage=${SHADER_STAGE} -O ${GLSL_FILE} -o ${SPV_FILE}
DEPENDS ${GLSL_FILE}
COMMENT "Compiling SPIR-V: ${FILE_NAME} -> ${SPV_NAME}"
VERBATIM
)
list(APPEND SPV_OUTPUTS ${SPV_FILE})
endforeach()
add_custom_target(${TARGET_NAME}_shaders ALL DEPENDS ${SPV_OUTPUTS})
add_dependencies(${TARGET_NAME} ${TARGET_NAME}_shaders)
endfunction()
# StormLib for MPQ extraction tool (not needed for main executable)
find_library(STORMLIB_LIBRARY NAMES StormLib stormlib storm)
find_path(STORMLIB_INCLUDE_DIR StormLib.h PATH_SUFFIXES StormLib)
# Include ImGui as a static library (we'll add the sources)
# Include ImGui as a static library (Vulkan backend)
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/imgui)
if(EXISTS ${IMGUI_DIR})
add_library(imgui STATIC
@ -93,19 +160,30 @@ if(EXISTS ${IMGUI_DIR})
${IMGUI_DIR}/imgui_widgets.cpp
${IMGUI_DIR}/imgui_demo.cpp
${IMGUI_DIR}/backends/imgui_impl_sdl2.cpp
${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp
${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp
)
target_include_directories(imgui PUBLIC
${IMGUI_DIR}
${IMGUI_DIR}/backends
)
target_link_libraries(imgui PUBLIC SDL2::SDL2 OpenGL::GL ${CMAKE_DL_LIBS})
target_compile_definitions(imgui PUBLIC IMGUI_IMPL_OPENGL_LOADER_GLEW)
target_link_libraries(imgui PUBLIC SDL2::SDL2 Vulkan::Vulkan ${CMAKE_DL_LIBS})
else()
message(WARNING "ImGui not found in extern/imgui. Clone it with:")
message(WARNING " git clone https://github.com/ocornut/imgui.git extern/imgui")
endif()
# vk-bootstrap (Vulkan device/instance setup)
set(VK_BOOTSTRAP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/vk-bootstrap)
if(EXISTS ${VK_BOOTSTRAP_DIR})
add_library(vk-bootstrap STATIC
${VK_BOOTSTRAP_DIR}/src/VkBootstrap.cpp
)
target_include_directories(vk-bootstrap PUBLIC ${VK_BOOTSTRAP_DIR}/src)
target_link_libraries(vk-bootstrap PUBLIC Vulkan::Vulkan)
else()
message(FATAL_ERROR "vk-bootstrap not found in extern/vk-bootstrap")
endif()
# Source files
set(WOWEE_SOURCES
# Core
@ -180,6 +258,15 @@ set(WOWEE_SOURCES
src/pipeline/terrain_mesh.cpp
# Rendering (Vulkan infrastructure)
src/rendering/vk_context.cpp
src/rendering/vk_utils.cpp
src/rendering/vk_shader.cpp
src/rendering/vk_texture.cpp
src/rendering/vk_buffer.cpp
src/rendering/vk_pipeline.cpp
src/rendering/vk_render_target.cpp
# Rendering
src/rendering/renderer.cpp
src/rendering/shader.cpp
@ -215,7 +302,7 @@ set(WOWEE_SOURCES
src/rendering/levelup_effect.cpp
src/rendering/charge_effect.cpp
src/rendering/loading_screen.cpp
src/rendering/video_player.cpp
$<$<BOOL:${HAVE_FFMPEG}>:${CMAKE_CURRENT_SOURCE_DIR}/src/rendering/video_player.cpp>
# UI
src/ui/ui_manager.cpp
@ -287,6 +374,13 @@ set(WOWEE_HEADERS
include/pipeline/dbc_loader.hpp
include/pipeline/terrain_mesh.hpp
include/rendering/vk_context.hpp
include/rendering/vk_utils.hpp
include/rendering/vk_shader.hpp
include/rendering/vk_texture.hpp
include/rendering/vk_buffer.hpp
include/rendering/vk_pipeline.hpp
include/rendering/vk_render_target.hpp
include/rendering/renderer.hpp
include/rendering/shader.hpp
include/rendering/texture.hpp
@ -343,19 +437,26 @@ if(TARGET opcodes-generate)
add_dependencies(wowee opcodes-generate)
endif()
# Compile GLSL shaders to SPIR-V
if(GLSLC)
compile_shaders(wowee)
endif()
# Include directories
target_include_directories(wowee PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/extern
${FFMPEG_INCLUDE_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}/extern/vk-bootstrap/src
)
if(HAVE_FFMPEG)
target_include_directories(wowee PRIVATE ${FFMPEG_INCLUDE_DIRS})
endif()
# Link libraries
target_link_libraries(wowee PRIVATE
SDL2::SDL2
OpenGL::GL
GLEW::GLEW
Vulkan::Vulkan
OpenSSL::SSL
OpenSSL::Crypto
Threads::Threads
@ -363,9 +464,20 @@ target_link_libraries(wowee PRIVATE
${CMAKE_DL_LIBS}
)
target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
if (FFMPEG_LIBRARY_DIRS)
target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS})
# GL/GLEW linked temporarily for unconverted sub-renderers (removed in Phase 7)
if(TARGET OpenGL::GL)
target_link_libraries(wowee PRIVATE OpenGL::GL)
endif()
if(TARGET GLEW::GLEW)
target_link_libraries(wowee PRIVATE GLEW::GLEW)
endif()
if(HAVE_FFMPEG)
target_compile_definitions(wowee PRIVATE HAVE_FFMPEG)
target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
if(FFMPEG_LIBRARY_DIRS)
target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS})
endif()
endif()
# Platform-specific libraries
@ -385,6 +497,11 @@ if(TARGET imgui)
target_link_libraries(wowee PRIVATE imgui)
endif()
# Link vk-bootstrap
if(TARGET vk-bootstrap)
target_link_libraries(wowee PRIVATE vk-bootstrap)
endif()
# Link Unicorn if available
if(HAVE_UNICORN)
target_link_libraries(wowee PRIVATE ${UNICORN_LIBRARY})
@ -406,6 +523,47 @@ else()
target_compile_options(wowee PRIVATE -Wall -Wextra -Wpedantic)
endif()
# Debug build flags
if(MSVC)
# /ZI — Edit-and-Continue debug info (works with hot-reload in VS 2022)
# /RTC1 — stack-frame and uninitialised-variable runtime checks (Debug only)
# /sdl — additional SDL security checks
# /Od — disable optimisation so stepping matches source lines exactly
target_compile_options(wowee PRIVATE
$<$<CONFIG:Debug>:/ZI /RTC1 /sdl /Od>
)
# Ensure the linker emits a .pdb alongside the .exe for every config
target_link_options(wowee PRIVATE
$<$<CONFIG:Debug>:/DEBUG:FULL>
$<$<CONFIG:RelWithDebInfo>:/DEBUG:FASTLINK>
)
else()
# -g3 — maximum DWARF debug info (includes macro definitions)
# -Og — optimise for debugging (better than -O0, keeps most frames)
# -fno-omit-frame-pointer — preserve frame pointers so stack traces are clean
target_compile_options(wowee PRIVATE
$<$<CONFIG:Debug>:-g3 -Og -fno-omit-frame-pointer>
$<$<CONFIG:RelWithDebInfo>:-g -fno-omit-frame-pointer>
)
endif()
# AddressSanitizer — catch buffer overflows, use-after-free, etc.
# Enable with: cmake ... -DWOWEE_ENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
if(WOWEE_ENABLE_ASAN)
if(MSVC)
target_compile_options(wowee PRIVATE /fsanitize=address)
# ASAN on MSVC requires the dynamic CRT (/MD or /MDd)
target_compile_options(wowee PRIVATE
$<$<CONFIG:Debug>:/MDd>
$<$<CONFIG:Release>:/MD>
)
else()
target_compile_options(wowee PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(wowee PRIVATE -fsanitize=address)
endif()
message(STATUS "AddressSanitizer: ENABLED")
endif()
# Release build optimizations
include(CheckIPOSupported)
check_ipo_supported(RESULT _ipo_supported OUTPUT _ipo_error)
@ -420,9 +578,27 @@ if(NOT MSVC)
target_compile_options(wowee PRIVATE $<$<CONFIG:Release>:-fvisibility=hidden -fvisibility-inlines-hidden>)
endif()
# Copy assets to build directory
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/assets
DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
# Copy assets next to the executable (runs every build, not just configure).
# Uses $<TARGET_FILE_DIR:wowee> so MSVC multi-config generators place assets
# in bin/Debug/ or bin/Release/ alongside the exe, not the common bin/ parent.
add_custom_command(TARGET wowee POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/assets
$<TARGET_FILE_DIR:wowee>/assets
COMMENT "Syncing assets to $<TARGET_FILE_DIR:wowee>/assets"
)
# On Windows, SDL 2.28+ uses LoadLibraryExW with LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
# which does NOT include System32. Copy vulkan-1.dll into the output directory so
# SDL_Vulkan_LoadLibrary can locate it without needing a full system PATH search.
if(WIN32)
add_custom_command(TARGET wowee POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$ENV{SystemRoot}/System32/vulkan-1.dll"
"$<TARGET_FILE_DIR:wowee>/vulkan-1.dll"
COMMENT "Copying vulkan-1.dll to output directory"
)
endif()
# Install targets
install(TARGETS wowee
@ -482,6 +658,9 @@ if(STORMLIB_LIBRARY AND STORMLIB_INCLUDE_DIR)
ZLIB::ZLIB
Threads::Threads
)
if(WIN32)
target_link_libraries(asset_extract PRIVATE wininet bz2)
endif()
set_target_properties(asset_extract PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
@ -575,6 +754,7 @@ message(STATUS " Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS " SDL2: ${SDL2_VERSION}")
message(STATUS " OpenSSL: ${OPENSSL_VERSION}")
message(STATUS " ImGui: ${IMGUI_DIR}")
message(STATUS " ASAN: ${WOWEE_ENABLE_ASAN}")
message(STATUS "")
# ---- CPack packaging ----
@ -628,7 +808,7 @@ chmod +x /usr/local/bin/wowee
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Wowee")
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"libsdl2-2.0-0, libglew2.2 | libglew2.1, libssl3, zlib1g")
"libsdl2-2.0-0, libvulkan1, libssl3, zlib1g")
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_CURRENT_BINARY_DIR}/packaging/postinst;${CMAKE_CURRENT_BINARY_DIR}/packaging/prerm")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")

View file

@ -4,22 +4,23 @@
<img src="assets/Wowee.png" alt="Wowee Logo" width="240" />
</p>
A native C++ World of Warcraft client with a custom OpenGL renderer.
A native C++ World of Warcraft client with a custom Vulkan renderer.
[![Sponsor](https://img.shields.io/github/sponsors/Kelsidavis?label=Sponsor&logo=GitHub)](https://github.com/sponsors/Kelsidavis)
[![Discord](https://img.shields.io/discord/1?label=Discord&logo=discord)](https://discord.gg/SDqjA79B)
[![Watch the video](https://img.youtube.com/vi/Pd9JuYYxu0o/maxresdefault.jpg)](https://youtu.be/Pd9JuYYxu0o)
[![Watch the video](https://img.youtube.com/vi/J4NXegzqWSQ/maxresdefault.jpg)](https://youtu.be/J4NXegzqWSQ)
Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. All three expansions are broadly functional with roughly even support.
Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
> **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction.
## Status & Direction (2026-02-18)
- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all broadly supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par — no single one is significantly more complete than the others.
- **Tested against**: AzerothCore, TrinityCore, and ChromieCraft.
- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par — no single one is significantly more complete than the others.
- **Tested against**: AzerothCore, TrinityCore, and Mangos.
- **Current focus**: protocol correctness across server variants, visual accuracy (M2/WMO edge cases, equipment textures), and multi-expansion coverage.
- **Warden**: Full module execution via Unicorn Engine CPU emulation. Decrypts (RC4→RSA→zlib), parses and relocates the PE module, executes via x86 emulation with Windows API interception. Module cache at `~/.local/share/wowee/warden_cache/`.
@ -27,18 +28,10 @@ Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. All three
### Rendering Engine
- **Terrain** -- Multi-tile streaming with async loading, texture splatting (4 layers), frustum culling
- **Water** -- Animated surfaces, reflections, refractions, Fresnel effect
- **Sky System** -- WoW-accurate DBC-driven lighting with skybox authority
- **Skybox** -- Camera-locked celestial sphere (M2 model support, gradient fallback)
- **Celestial Bodies** -- Sun (lighting-driven), White Lady + Blue Child (Azeroth's two moons)
- **Moon Phases** -- Game time-driven deterministic phases when server time is available (fallback: local cycling for development)
- **Stars** -- Baked into skybox assets (procedural fallback for development/debug only)
- **Atmosphere** -- Procedural clouds (FBM noise), lens flare with chromatic aberration, cloud/fog star occlusion
- **Weather** -- Rain and snow particle systems (2000 particles, camera-relative)
- **Characters** -- Skeletal animation with GPU vertex skinning (256 bones), race-aware textures
- **Buildings** -- WMO renderer with multi-material batches, frustum culling, 160-unit distance culling
- **Particles** -- M2 particle emitters with WotLK struct parsing, billboarded glow effects
- **Post-Processing** -- HDR, tonemapping, shadow mapping (2048x2048)
### Asset Pipeline
- Extracted loose-file **`Data/`** tree indexed by **`manifest.json`** (fast lookup + caching)
@ -71,21 +64,26 @@ Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. All three
```bash
# Ubuntu/Debian
sudo apt install libsdl2-dev libglew-dev libglm-dev \
libssl-dev cmake build-essential \
libunicorn-dev \ # for Warden module execution
libstorm-dev # for asset_extract
sudo apt install libsdl2-dev libglm-dev libssl-dev \
libvulkan-dev vulkan-tools glslc \
libavformat-dev libavcodec-dev libswscale-dev libavutil-dev \
zlib1g-dev cmake build-essential libx11-dev \
libunicorn-dev \ # optional: Warden module execution
libstorm-dev # optional: asset_extract tool
# Fedora
sudo dnf install SDL2-devel glew-devel glm-devel \
openssl-devel cmake gcc-c++ \
unicorn-devel \ # for Warden module execution
StormLib-devel # for asset_extract
sudo dnf install SDL2-devel glm-devel openssl-devel \
vulkan-devel vulkan-tools glslc \
ffmpeg-devel zlib-devel cmake gcc-c++ libX11-devel \
unicorn-devel \ # optional: Warden module execution
StormLib-devel # optional: asset_extract tool
# Arch
sudo pacman -S sdl2 glew glm openssl cmake base-devel \
unicorn \ # for Warden module execution
stormlib # for asset_extract
sudo pacman -S sdl2 glm openssl \
vulkan-devel vulkan-tools shaderc \
ffmpeg zlib cmake base-devel libx11 \
unicorn # optional: Warden module execution
# StormLib: install from AUR for asset_extract tool
```
### Container build
@ -208,15 +206,12 @@ make -j$(nproc)
## Technical Details
- **Graphics**: OpenGL 3.3 Core, GLSL 330, forward rendering with post-processing
- **Performance**: 60 FPS (vsync), ~50k triangles/frame, ~30 draw calls, <10% GPU
- **Platform**: Linux (primary), C++20, CMake 3.15+
- **Dependencies**: SDL2, OpenGL/GLEW, GLM, OpenSSL, ImGui, FFmpeg, Unicorn Engine (StormLib for asset extraction tooling)
- **Dependencies**: SDL2, Vulkan, GLM, OpenSSL, ImGui, FFmpeg, Unicorn Engine (StormLib for asset extraction tooling)
- **Architecture**: Modular design with clear separation (core, rendering, networking, game logic, asset pipeline, UI, audio)
- **Networking**: Non-blocking TCP, SRP6a authentication, RC4 encryption, WoW 3.3.5a protocol
- **Asset Loading**: Extracted loose-file tree + `manifest.json` indexing, async terrain streaming, overlay layers
- **Sky System**: WoW-accurate DBC-driven architecture
- **Skybox Authority**: Stars baked into M2 sky dome models (not procedurally generated)
- **Lore-Accurate Moons**: White Lady (30-day cycle) + Blue Child (27-day cycle)
- **Deterministic Phases**: Computed from server game time when available (fallback: local time/dev cycling)
- **Camera-Locked**: Sky dome uses rotation-only transform (translation ignored)
@ -238,8 +233,4 @@ This project does not include any Blizzard Entertainment proprietary data, asset
## Known Issues
### Water Rendering
- **Stormwind Canal Overflow**: Canal water surfaces extend spatially beyond their intended boundaries, causing water to appear in tunnels, buildings, and the park. This is due to oversized water mesh extents in the WoW data files.
- **Current Workaround**: Water heights are lowered by 1 unit in Stormwind (tiles 28-50, 28-52) for surfaces above 94 units, with a 20-unit exclusion zone around the moonwell (-8755.9, 1108.9, 96.1). This hides most problem water while keeping canals and the moonwell functional.
- **Limitation**: Some park water may still be visible. The workaround uses hardcoded coordinates and height thresholds rather than fixing the root cause.
- **Proper Fix**: Would require trimming water surface meshes to actual boundaries in ADT/WMO data, or implementing spatial clipping at render time.
MANY issues this is actively under development

BIN
assets/krayonload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
assets/krayonsignin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View file

@ -0,0 +1,49 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(set = 1, binding = 0) uniform sampler2D uTexture;
layout(set = 1, binding = 1) uniform BasicMaterial {
vec4 color;
vec3 lightPos;
int useTexture;
};
layout(location = 0) in vec3 FragPos;
layout(location = 1) in vec3 Normal;
layout(location = 2) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
void main() {
vec3 ambient = 0.3 * vec3(1.0);
vec3 norm = normalize(Normal);
vec3 lightDir2 = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir2), 0.0);
vec3 diffuse = diff * vec3(1.0);
vec3 viewDir2 = normalize(viewPos.xyz - FragPos);
vec3 reflectDir = reflect(-lightDir2, norm);
float spec = pow(max(dot(viewDir2, reflectDir), 0.0), 32.0);
vec3 specular = 0.5 * spec * vec3(1.0);
vec3 result = ambient + diffuse + specular;
if (useTexture != 0) {
outColor = texture(uTexture, TexCoord) * vec4(result, 1.0);
} else {
outColor = color * vec4(result, 1.0);
}
}

Binary file not shown.

View file

@ -0,0 +1,34 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
mat4 model;
} push;
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
layout(location = 0) out vec3 FragPos;
layout(location = 1) out vec3 Normal;
layout(location = 2) out vec2 TexCoord;
void main() {
vec4 worldPos = push.model * vec4(aPosition, 1.0);
FragPos = worldPos.xyz;
Normal = mat3(push.model) * aNormal;
TexCoord = aTexCoord;
gl_Position = projection * view * worldPos;
}

Binary file not shown.

View file

@ -0,0 +1,64 @@
#version 450
layout(push_constant) uniform Push {
mat4 model;
vec4 celestialColor; // xyz = color, w = unused
float intensity;
float moonPhase;
float animTime;
} push;
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
float valueNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = fract(sin(dot(i, vec2(127.1, 311.7))) * 43758.5453);
float b = fract(sin(dot(i + vec2(1.0, 0.0), vec2(127.1, 311.7))) * 43758.5453);
float c = fract(sin(dot(i + vec2(0.0, 1.0), vec2(127.1, 311.7))) * 43758.5453);
float d = fract(sin(dot(i + vec2(1.0, 1.0), vec2(127.1, 311.7))) * 43758.5453);
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
void main() {
vec2 uv = TexCoord - 0.5;
float dist = length(uv);
// Hard circular cutoff — nothing beyond radius 0.35
if (dist > 0.35) discard;
// Hard disc with smooth edge
float disc = smoothstep(0.35, 0.28, dist);
// Soft glow confined within cutoff radius
float glow = exp(-dist * dist * 40.0) * 0.5;
// Combine disc and glow
float alpha = max(disc, glow) * push.intensity;
// Smooth fade to zero at cutoff boundary
float edgeFade = 1.0 - smoothstep(0.25, 0.35, dist);
alpha *= edgeFade;
vec3 color = push.celestialColor.rgb;
// Animated haze/turbulence overlay for the sun disc
if (push.intensity > 0.5) {
float noise = valueNoise(uv * 8.0 + vec2(push.animTime * 0.3, push.animTime * 0.2));
float noise2 = valueNoise(uv * 16.0 - vec2(push.animTime * 0.5, push.animTime * 0.15));
float turbulence = (noise * 0.6 + noise2 * 0.4) * disc;
color += vec3(turbulence * 0.3, turbulence * 0.15, 0.0);
}
// Moon phase shadow (only applied when intensity < 0.5, i.e. for moons)
float phaseX = uv.x * 2.0 + push.moonPhase;
float phaseShadow = smoothstep(-0.1, 0.1, phaseX);
alpha *= mix(phaseShadow, 1.0, step(0.5, push.intensity));
if (alpha < 0.01) discard;
// Pre-multiply for additive blending: RGB is the light contribution
outColor = vec4(color * alpha, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,34 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
mat4 model;
vec4 celestialColor; // xyz = color, w = unused
float intensity;
float moonPhase;
float animTime;
} push;
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
layout(location = 0) out vec2 TexCoord;
void main() {
TexCoord = aTexCoord;
// Sky object: remove camera translation so celestial bodies are at infinite distance
mat4 rotView = mat4(mat3(view));
gl_Position = projection * rotView * push.model * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,187 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(set = 1, binding = 0) uniform sampler2D uTexture;
layout(set = 1, binding = 1) uniform CharMaterial {
float opacity;
int alphaTest;
int colorKeyBlack;
int unlit;
float emissiveBoost;
vec3 emissiveTint;
float specularIntensity;
int enableNormalMap;
int enablePOM;
float pomScale;
int pomMaxSamples;
float heightMapVariance;
float normalMapStrength;
};
layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
layout(location = 0) in vec3 FragPos;
layout(location = 1) in vec3 Normal;
layout(location = 2) in vec2 TexCoord;
layout(location = 3) in vec3 Tangent;
layout(location = 4) in vec3 Bitangent;
layout(location = 0) out vec4 outColor;
const float SHADOW_TEXEL = 1.0 / 4096.0;
float sampleShadowPCF(sampler2DShadow smap, vec3 coords) {
float shadow = 0.0;
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z));
}
}
return shadow / 9.0;
}
// LOD factor from screen-space UV derivatives
float computeLodFactor() {
vec2 dx = dFdx(TexCoord);
vec2 dy = dFdy(TexCoord);
float texelDensity = max(dot(dx, dx), dot(dy, dy));
return smoothstep(0.0001, 0.005, texelDensity);
}
// Parallax Occlusion Mapping with angle-adaptive sampling
vec2 parallaxOcclusionMap(vec2 uv, vec3 viewDirTS, float lodFactor) {
float VdotN = abs(viewDirTS.z);
if (VdotN < 0.15) return uv;
float angleFactor = clamp(VdotN, 0.15, 1.0);
int maxS = pomMaxSamples;
int minS = max(maxS / 4, 4);
int numSamples = int(mix(float(minS), float(maxS), angleFactor));
numSamples = int(mix(float(minS), float(numSamples), 1.0 - lodFactor));
float layerDepth = 1.0 / float(numSamples);
float currentLayerDepth = 0.0;
vec2 P = viewDirTS.xy / max(VdotN, 0.15) * pomScale;
float maxOffset = pomScale * 3.0;
P = clamp(P, vec2(-maxOffset), vec2(maxOffset));
vec2 deltaUV = P / float(numSamples);
vec2 currentUV = uv;
float currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
for (int i = 0; i < 64; i++) {
if (i >= numSamples || currentLayerDepth >= currentDepthMapValue) break;
currentUV -= deltaUV;
currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
currentLayerDepth += layerDepth;
}
vec2 prevUV = currentUV + deltaUV;
float afterDepth = currentDepthMapValue - currentLayerDepth;
float beforeDepth = (1.0 - texture(uNormalHeightMap, prevUV).a) - currentLayerDepth + layerDepth;
float weight = afterDepth / (afterDepth - beforeDepth + 0.0001);
vec2 result = mix(currentUV, prevUV, weight);
float fadeFactor = smoothstep(0.15, 0.35, VdotN);
return mix(uv, result, fadeFactor);
}
void main() {
float lodFactor = computeLodFactor();
vec3 vertexNormal = normalize(Normal);
if (!gl_FrontFacing) vertexNormal = -vertexNormal;
vec2 finalUV = TexCoord;
// Build TBN matrix
vec3 T = normalize(Tangent);
vec3 B = normalize(Bitangent);
vec3 N = vertexNormal;
mat3 TBN = mat3(T, B, N);
if (enablePOM != 0 && heightMapVariance > 0.001 && lodFactor < 0.99) {
mat3 TBN_inv = transpose(TBN);
vec3 viewDirWorld = normalize(viewPos.xyz - FragPos);
vec3 viewDirTS = TBN_inv * viewDirWorld;
finalUV = parallaxOcclusionMap(TexCoord, viewDirTS, lodFactor);
}
vec4 texColor = texture(uTexture, finalUV);
if (alphaTest != 0 && texColor.a < 0.5) discard;
if (colorKeyBlack != 0) {
float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
float ck = smoothstep(0.12, 0.30, lum);
texColor.a *= ck;
if (texColor.a < 0.01) discard;
}
// Compute normal (with normal mapping if enabled)
vec3 norm = vertexNormal;
if (enableNormalMap != 0 && lodFactor < 0.99 && normalMapStrength > 0.001) {
vec3 mapNormal = texture(uNormalHeightMap, finalUV).rgb * 2.0 - 1.0;
mapNormal.xy *= normalMapStrength;
mapNormal = normalize(mapNormal);
vec3 worldNormal = normalize(TBN * mapNormal);
if (!gl_FrontFacing) worldNormal = -worldNormal;
float blendFactor = max(lodFactor, 1.0 - normalMapStrength);
norm = normalize(mix(worldNormal, vertexNormal, blendFactor));
}
vec3 result;
if (unlit != 0) {
vec3 warm = emissiveTint * emissiveBoost;
result = texColor.rgb * (1.0 + warm);
} else {
vec3 ldir = normalize(-lightDir.xyz);
float diff = max(dot(norm, ldir), 0.0);
vec3 viewDir = normalize(viewPos.xyz - FragPos);
vec3 halfDir = normalize(ldir + viewDir);
float spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity;
float shadow = 1.0;
if (shadowParams.x > 0.5) {
float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir)));
vec3 biasedPos = FragPos + norm * normalOffset;
vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w;
proj.xy = proj.xy * 0.5 + 0.5;
if (proj.x >= 0.0 && proj.x <= 1.0 &&
proj.y >= 0.0 && proj.y <= 1.0 &&
proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
}
shadow = mix(1.0, shadow, shadowParams.y);
}
result = ambientColor.rgb * texColor.rgb
+ shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb);
}
float dist = length(viewPos.xyz - FragPos);
float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
result = mix(fogColor.rgb, result, fogFactor);
outColor = vec4(result, texColor.a * opacity);
}

Binary file not shown.

View file

@ -0,0 +1,63 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
mat4 model;
} push;
layout(set = 2, binding = 0) readonly buffer BoneSSBO {
mat4 bones[];
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec4 aBoneWeights;
layout(location = 2) in ivec4 aBoneIndices;
layout(location = 3) in vec3 aNormal;
layout(location = 4) in vec2 aTexCoord;
layout(location = 5) in vec4 aTangent;
layout(location = 0) out vec3 FragPos;
layout(location = 1) out vec3 Normal;
layout(location = 2) out vec2 TexCoord;
layout(location = 3) out vec3 Tangent;
layout(location = 4) out vec3 Bitangent;
void main() {
mat4 skinMat = bones[aBoneIndices.x] * aBoneWeights.x
+ bones[aBoneIndices.y] * aBoneWeights.y
+ bones[aBoneIndices.z] * aBoneWeights.z
+ bones[aBoneIndices.w] * aBoneWeights.w;
vec4 skinnedPos = skinMat * vec4(aPos, 1.0);
vec3 skinnedNorm = mat3(skinMat) * aNormal;
vec3 skinnedTan = mat3(skinMat) * aTangent.xyz;
vec4 worldPos = push.model * skinnedPos;
mat3 modelMat3 = mat3(push.model);
FragPos = worldPos.xyz;
Normal = modelMat3 * skinnedNorm;
TexCoord = aTexCoord;
// Gram-Schmidt re-orthogonalize tangent w.r.t. normal
vec3 N = normalize(Normal);
vec3 T = normalize(modelMat3 * skinnedTan);
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T) * aTangent.w;
Tangent = T;
Bitangent = B;
gl_Position = projection * view * worldPos;
}

Binary file not shown.

View file

@ -0,0 +1,19 @@
#version 450
layout(set = 1, binding = 0) uniform sampler2D uTexture;
layout(set = 1, binding = 1) uniform ShadowParams {
int alphaTest;
int colorKeyBlack;
};
layout(location = 0) in vec2 TexCoord;
void main() {
vec4 texColor = texture(uTexture, TexCoord);
if (alphaTest != 0 && texColor.a < 0.5) discard;
if (colorKeyBlack != 0) {
float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
if (lum < 0.12) discard;
}
}

Binary file not shown.

View file

@ -0,0 +1,27 @@
#version 450
layout(push_constant) uniform Push {
mat4 lightSpaceMatrix;
mat4 model;
} push;
layout(set = 2, binding = 0) readonly buffer BoneSSBO {
mat4 bones[];
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec4 aBoneWeights;
layout(location = 2) in ivec4 aBoneIndices;
layout(location = 3) in vec2 aTexCoord;
layout(location = 0) out vec2 TexCoord;
void main() {
mat4 skinMat = bones[aBoneIndices.x] * aBoneWeights.x
+ bones[aBoneIndices.y] * aBoneWeights.y
+ bones[aBoneIndices.z] * aBoneWeights.z
+ bones[aBoneIndices.w] * aBoneWeights.w;
vec4 skinnedPos = skinMat * vec4(aPos, 1.0);
TexCoord = aTexCoord;
gl_Position = push.lightSpaceMatrix * push.model * skinnedPos;
}

Binary file not shown.

View file

@ -0,0 +1,13 @@
#version 450
layout(location = 0) in float vAlpha;
layout(location = 0) out vec4 outColor;
void main() {
vec2 p = gl_PointCoord - vec2(0.5);
float dist = length(p);
if (dist > 0.5) discard;
float alpha = smoothstep(0.5, 0.1, dist) * vAlpha * 0.45;
outColor = vec4(0.65, 0.55, 0.40, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,26 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in float aSize;
layout(location = 2) in float aAlpha;
layout(location = 0) out float vAlpha;
void main() {
gl_PointSize = aSize;
vAlpha = aAlpha;
gl_Position = projection * view * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,16 @@
#version 450
layout(location = 0) in float vAlpha;
layout(location = 1) in float vHeat;
layout(location = 2) in float vHeight;
layout(location = 0) out vec4 outColor;
void main() {
vec3 top = vec3(1.0, 0.2, 0.0);
vec3 mid = vec3(1.0, 0.5, 0.0);
vec3 color = mix(mid, top, vHeight);
color = mix(color, vec3(1.0, 0.8, 0.3), vHeat * 0.5);
float alpha = vAlpha * smoothstep(0.0, 0.3, vHeight);
outColor = vec4(color, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,30 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in float aAlpha;
layout(location = 2) in float aHeat;
layout(location = 3) in float aHeight;
layout(location = 0) out float vAlpha;
layout(location = 1) out float vHeat;
layout(location = 2) out float vHeight;
void main() {
vAlpha = aAlpha;
vHeat = aHeat;
vHeight = aHeight;
gl_Position = projection * view * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,104 @@
#version 450
layout(push_constant) uniform Push {
vec4 cloudColor; // xyz = DBC-derived base cloud color, w = unused
vec4 sunDirDensity; // xyz = sun direction, w = density
vec4 windAndLight; // x = windOffset, y = sunIntensity, z = ambient, w = unused
} push;
layout(location = 0) in vec3 vWorldDir;
layout(location = 0) out vec4 outColor;
// --- Gradient noise (smoother than hash-based) ---
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453);
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// Quintic interpolation for smoother results
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i + vec2(0.0, 0.0)) * 2.0 - 1.0, f - vec2(0.0, 0.0));
float b = dot(hash2(i + vec2(1.0, 0.0)) * 2.0 - 1.0, f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)) * 2.0 - 1.0, f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)) * 2.0 - 1.0, f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 0.5 + 0.5;
}
float fbm(vec2 p) {
float val = 0.0;
float amp = 0.5;
for (int i = 0; i < 6; i++) {
val += amp * gradientNoise(p);
p *= 2.0;
amp *= 0.5;
}
return val;
}
void main() {
vec3 dir = normalize(vWorldDir);
float altitude = dir.z;
if (altitude < 0.0) discard;
vec3 sunDir = push.sunDirDensity.xyz;
float density = push.sunDirDensity.w;
float windOffset = push.windAndLight.x;
float sunIntensity = push.windAndLight.y;
float ambient = push.windAndLight.z;
vec2 uv = dir.xy / (altitude + 0.001);
uv += windOffset;
// --- 6-octave FBM for cloud shape ---
float cloud1 = fbm(uv * 0.8);
float cloud2 = fbm(uv * 1.6 + 5.0);
float cloud = cloud1 * 0.7 + cloud2 * 0.3;
// Coverage control: base coverage with detail erosion
float baseCoverage = smoothstep(0.30, 0.55, cloud);
float detailErosion = gradientNoise(uv * 4.0);
cloud = baseCoverage * smoothstep(0.2, 0.5, detailErosion);
cloud *= density;
// Horizon fade
float horizonFade = smoothstep(0.0, 0.15, altitude);
cloud *= horizonFade;
if (cloud < 0.01) discard;
// --- Sun lighting on clouds ---
// Sun dot product for view-relative brightness
float sunDot = max(dot(vec3(0.0, 0.0, 1.0), sunDir), 0.0);
// Self-shadowing: sample noise offset toward sun direction, darken if occluded
float lightSample = fbm((uv + sunDir.xy * 0.05) * 0.8);
float shadow = smoothstep(0.3, 0.7, lightSample);
// Base lit color: mix dark (shadow) and bright (sunlit) based on shadow and sun
vec3 baseColor = push.cloudColor.rgb;
vec3 shadowColor = baseColor * (ambient * 0.8);
vec3 litColor = baseColor * (ambient + sunIntensity * 0.6);
vec3 cloudRgb = mix(shadowColor, litColor, shadow * sunDot);
// Add ambient fill so clouds aren't too dark
cloudRgb = mix(baseColor * ambient, cloudRgb, 0.7 + 0.3 * sunIntensity);
// --- Silver lining effect at cloud edges ---
float edgeLight = smoothstep(0.0, 0.3, cloud) * (1.0 - smoothstep(0.3, 0.8, cloud));
cloudRgb += vec3(1.0, 0.95, 0.9) * edgeLight * sunDot * sunIntensity * 0.4;
// --- Edge softness for alpha ---
float edgeSoftness = smoothstep(0.0, 0.3, cloud);
float alpha = cloud * edgeSoftness;
if (alpha < 0.01) discard;
outColor = vec4(cloudRgb, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,25 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(location = 0) in vec3 aPos;
layout(location = 0) out vec3 vWorldDir;
void main() {
vWorldDir = aPos;
mat4 rotView = mat4(mat3(view));
vec4 pos = projection * rotView * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}

Binary file not shown.

View file

@ -0,0 +1,22 @@
#version 450
layout(push_constant) uniform Push {
vec2 position;
float size;
float aspectRatio;
vec4 color; // rgb + brightness in w
} push;
layout(location = 0) in vec2 UV;
layout(location = 0) out vec4 outColor;
void main() {
vec2 center = UV - 0.5;
float dist = length(center);
float alpha = smoothstep(0.5, 0.0, dist);
float glow = exp(-dist * dist * 8.0) * 0.5;
alpha = max(alpha, glow) * push.color.w;
if (alpha < 0.01) discard;
outColor = vec4(push.color.rgb, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,19 @@
#version 450
layout(push_constant) uniform Push {
vec2 position;
float size;
float aspectRatio;
} push;
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 0) out vec2 UV;
void main() {
UV = aUV;
vec2 scaled = aPos * push.size;
scaled.x /= push.aspectRatio;
gl_Position = vec4(scaled + push.position, 0.0, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,10 @@
#version 450
layout(location = 0) in float vBrightness;
layout(location = 0) out vec4 outColor;
void main() {
vec3 color = mix(vec3(0.6, 0.8, 1.0), vec3(1.0), vBrightness * 0.5);
outColor = vec4(color, vBrightness);
}

Binary file not shown.

View file

@ -0,0 +1,27 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
float brightness;
} push;
layout(location = 0) in vec3 aPos;
layout(location = 0) out float vBrightness;
void main() {
vBrightness = push.brightness;
gl_Position = projection * view * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,11 @@
#version 450
layout(push_constant) uniform Push {
float intensity;
} push;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0, 1.0, 1.0, push.intensity * 0.6);
}

Binary file not shown.

View file

@ -0,0 +1,7 @@
#version 450
layout(location = 0) in vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}

Binary file not shown.

190
assets/shaders/m2.frag.glsl Normal file
View file

@ -0,0 +1,190 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(set = 1, binding = 0) uniform sampler2D uTexture;
layout(set = 1, binding = 2) uniform M2Material {
int hasTexture;
int alphaTest;
int colorKeyBlack;
float colorKeyThreshold;
int unlit;
int blendMode;
float fadeAlpha;
float interiorDarken;
float specularIntensity;
};
layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
layout(location = 0) in vec3 FragPos;
layout(location = 1) in vec3 Normal;
layout(location = 2) in vec2 TexCoord;
layout(location = 3) flat in vec3 InstanceOrigin;
layout(location = 4) in float ModelHeight;
layout(location = 0) out vec4 outColor;
const float SHADOW_TEXEL = 1.0 / 4096.0;
float sampleShadowPCF(sampler2DShadow smap, vec3 coords) {
float shadow = 0.0;
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z));
}
}
return shadow / 9.0;
}
// 4x4 Bayer dither matrix (normalized to 0..1)
float bayerDither4x4(ivec2 p) {
int idx = (p.x & 3) + (p.y & 3) * 4;
float m[16] = float[16](
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
);
return m[idx];
}
void main() {
vec4 texColor = hasTexture != 0 ? texture(uTexture, TexCoord) : vec4(1.0);
bool isFoliage = (alphaTest == 2);
// Fix DXT fringe: transparent edge texels have garbage (black) RGB.
// At low alpha the original RGB is untrustworthy — replace with the
// averaged color from nearby opaque texels (high mip). The lower
// the alpha the more we distrust the original color.
if (alphaTest != 0 && texColor.a > 0.01 && texColor.a < 1.0) {
vec3 mipColor = textureLod(uTexture, TexCoord, 4.0).rgb;
// trust = 0 at alpha 0, trust = 1 at alpha ~0.9
float trust = smoothstep(0.0, 0.9, texColor.a);
texColor.rgb = mix(mipColor, texColor.rgb, trust);
}
float alphaCutoff = 0.5;
if (alphaTest == 2) {
alphaCutoff = 0.4;
} else if (alphaTest == 3) {
alphaCutoff = 0.25;
} else if (alphaTest != 0) {
alphaCutoff = 0.4;
}
if (alphaTest != 0 && texColor.a < alphaCutoff) {
discard;
}
if (colorKeyBlack != 0) {
float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
if (lum < colorKeyThreshold) discard;
}
if (blendMode == 1 && texColor.a < 0.004) discard;
// Per-instance color variation (foliage only)
if (isFoliage) {
float hash = fract(sin(dot(InstanceOrigin.xy, vec2(127.1, 311.7))) * 43758.5453);
float hueShiftR = 1.0 + (hash - 0.5) * 0.16; // ±8% red
float hueShiftB = 1.0 + (fract(hash * 7.13) - 0.5) * 0.16; // ±8% blue
float brightness = 0.85 + hash * 0.30; // 85115%
texColor.rgb *= vec3(hueShiftR, 1.0, hueShiftB) * brightness;
}
vec3 norm = normalize(Normal);
bool foliageTwoSided = (alphaTest == 2);
if (!foliageTwoSided && !gl_FrontFacing) norm = -norm;
// Detail normal perturbation (foliage only) — UV-based only so wind doesn't cause flicker
if (isFoliage) {
float nx = sin(TexCoord.x * 12.0 + TexCoord.y * 5.3) * 0.10;
float ny = sin(TexCoord.y * 14.0 + TexCoord.x * 4.7) * 0.10;
norm = normalize(norm + vec3(nx, ny, 0.0));
}
vec3 ldir = normalize(-lightDir.xyz);
float nDotL = dot(norm, ldir);
float diff = foliageTwoSided ? abs(nDotL) : max(nDotL, 0.0);
vec3 result;
if (unlit != 0) {
result = texColor.rgb;
} else {
vec3 viewDir = normalize(viewPos.xyz - FragPos);
float spec = 0.0;
float shadow = 1.0;
if (!isFoliage) {
vec3 halfDir = normalize(ldir + viewDir);
spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity;
}
if (shadowParams.x > 0.5) {
float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir)));
vec3 biasedPos = FragPos + norm * normalOffset;
vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w;
proj.xy = proj.xy * 0.5 + 0.5;
if (proj.x >= 0.0 && proj.x <= 1.0 &&
proj.y >= 0.0 && proj.y <= 1.0 &&
proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
}
shadow = mix(1.0, shadow, shadowParams.y);
}
// Leaf subsurface scattering (foliage only) — uses stable normal, no FragPos dependency
vec3 sss = vec3(0.0);
if (isFoliage) {
float backLit = max(-nDotL, 0.0);
float viewDotLight = max(dot(viewDir, -ldir), 0.0);
float sssAmount = backLit * pow(viewDotLight, 4.0) * 0.35 * texColor.a;
sss = sssAmount * vec3(1.0, 0.9, 0.5) * lightColor.rgb;
}
result = ambientColor.rgb * texColor.rgb
+ shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb)
+ sss;
if (interiorDarken > 0.0) {
result *= mix(1.0, 0.5, interiorDarken);
}
}
// Canopy ambient occlusion (foliage only)
if (isFoliage) {
float normalizedHeight = clamp(ModelHeight / 18.0, 0.0, 1.0);
float aoFactor = mix(0.55, 1.0, smoothstep(0.0, 0.6, normalizedHeight));
result *= aoFactor;
}
float dist = length(viewPos.xyz - FragPos);
float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
result = mix(fogColor.rgb, result, fogFactor);
float outAlpha = texColor.a * fadeAlpha;
// Cutout materials should not remain partially transparent after discard,
// otherwise foliage cards look view-dependent.
if (alphaTest != 0 || colorKeyBlack != 0) {
outAlpha = fadeAlpha;
}
// Foliage cutout should stay opaque after alpha discard to avoid
// view-angle translucency artifacts.
if (alphaTest == 2 || alphaTest == 3) {
outAlpha = 1.0 * fadeAlpha;
}
outColor = vec4(result, outAlpha);
}

BIN
assets/shaders/m2.frag.spv Normal file

Binary file not shown.

View file

@ -0,0 +1,91 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
mat4 model;
vec2 uvOffset;
int texCoordSet;
int useBones;
int isFoliage;
} push;
layout(set = 2, binding = 0) readonly buffer BoneSSBO {
mat4 bones[];
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in vec4 aBoneWeights;
layout(location = 4) in vec4 aBoneIndicesF;
layout(location = 5) in vec2 aTexCoord2;
layout(location = 0) out vec3 FragPos;
layout(location = 1) out vec3 Normal;
layout(location = 2) out vec2 TexCoord;
layout(location = 3) flat out vec3 InstanceOrigin;
layout(location = 4) out float ModelHeight;
void main() {
vec4 pos = vec4(aPos, 1.0);
vec4 norm = vec4(aNormal, 0.0);
if (push.useBones != 0) {
ivec4 bi = ivec4(aBoneIndicesF);
mat4 skinMat = bones[bi.x] * aBoneWeights.x
+ bones[bi.y] * aBoneWeights.y
+ bones[bi.z] * aBoneWeights.z
+ bones[bi.w] * aBoneWeights.w;
pos = skinMat * pos;
norm = skinMat * norm;
}
// Wind animation for foliage
if (push.isFoliage != 0) {
float windTime = fogParams.z;
vec3 worldRef = push.model[3].xyz;
float heightFactor = clamp(pos.z / 20.0, 0.0, 1.0);
heightFactor *= heightFactor; // quadratic — base stays grounded
// Layer 1: Trunk sway — slow, large amplitude
float trunkPhase = windTime * 0.8 + dot(worldRef.xy, vec2(0.1, 0.13));
float trunkSwayX = sin(trunkPhase) * 0.35 * heightFactor;
float trunkSwayY = cos(trunkPhase * 0.7) * 0.25 * heightFactor;
// Layer 2: Branch sway — medium frequency, per-branch phase
float branchPhase = windTime * 1.7 + dot(worldRef.xy, vec2(0.37, 0.71));
float branchSwayX = sin(branchPhase + pos.y * 0.4) * 0.15 * heightFactor;
float branchSwayY = cos(branchPhase * 1.1 + pos.x * 0.3) * 0.12 * heightFactor;
// Layer 3: Leaf flutter — fast, small amplitude, per-vertex
float leafPhase = windTime * 4.5 + dot(aPos, vec3(1.7, 2.3, 0.9));
float leafFlutterX = sin(leafPhase) * 0.06 * heightFactor;
float leafFlutterY = cos(leafPhase * 1.3) * 0.05 * heightFactor;
pos.x += trunkSwayX + branchSwayX + leafFlutterX;
pos.y += trunkSwayY + branchSwayY + leafFlutterY;
}
vec4 worldPos = push.model * pos;
FragPos = worldPos.xyz;
Normal = mat3(push.model) * norm.xyz;
TexCoord = (push.texCoordSet == 1 ? aTexCoord2 : aTexCoord) + push.uvOffset;
InstanceOrigin = push.model[3].xyz;
ModelHeight = pos.z;
gl_Position = projection * view * worldPos;
}

BIN
assets/shaders/m2.vert.spv Normal file

Binary file not shown.

View file

@ -0,0 +1,30 @@
#version 450
layout(set = 1, binding = 0) uniform sampler2D uTexture;
layout(push_constant) uniform Push {
vec2 tileCount;
int alphaKey;
} push;
layout(location = 0) in vec4 vColor;
layout(location = 1) in float vTile;
layout(location = 0) out vec4 outColor;
void main() {
vec2 p = gl_PointCoord;
float tile = floor(vTile);
float tx = mod(tile, push.tileCount.x);
float ty = floor(tile / push.tileCount.x);
vec2 uv = (vec2(tx, ty) + p) / push.tileCount;
vec4 texColor = texture(uTexture, uv);
if (push.alphaKey != 0) {
float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
if (lum < 0.05) discard;
}
float edge = smoothstep(0.5, 0.4, length(p - 0.5));
outColor = texColor * vColor * vec4(vec3(1.0), edge);
}

Binary file not shown.

View file

@ -0,0 +1,31 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec4 aColor;
layout(location = 2) in float aSize;
layout(location = 3) in float aTile;
layout(location = 0) out vec4 vColor;
layout(location = 1) out float vTile;
void main() {
vec4 viewPos4 = view * vec4(aPos, 1.0);
float dist = -viewPos4.z;
gl_PointSize = clamp(aSize * 500.0 / max(dist, 1.0), 1.0, 128.0);
vColor = aColor;
vTile = aTile;
gl_Position = projection * viewPos4;
}

Binary file not shown.

View file

@ -0,0 +1,25 @@
#version 450
layout(location = 0) in float vLifeRatio;
layout(location = 1) in float vIsSpark;
layout(location = 0) out vec4 outColor;
void main() {
vec2 p = gl_PointCoord - vec2(0.5);
float dist = length(p);
if (dist > 0.5) discard;
if (vIsSpark > 0.5) {
float glow = smoothstep(0.5, 0.0, dist);
float life = 1.0 - vLifeRatio;
vec3 color = mix(vec3(1.0, 0.6, 0.1), vec3(1.0, 0.2, 0.0), vLifeRatio);
outColor = vec4(color * glow, glow * life);
} else {
float edge = smoothstep(0.5, 0.3, dist);
float fadeIn = smoothstep(0.0, 0.2, vLifeRatio);
float fadeOut = 1.0 - smoothstep(0.6, 1.0, vLifeRatio);
float alpha = edge * fadeIn * fadeOut * 0.4;
outColor = vec4(vec3(0.5), alpha);
}
}

Binary file not shown.

View file

@ -0,0 +1,36 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
float screenHeight;
} push;
layout(location = 0) in vec3 aPos;
layout(location = 1) in float aLifeRatio;
layout(location = 2) in float aSize;
layout(location = 3) in float aIsSpark;
layout(location = 0) out float vLifeRatio;
layout(location = 1) out float vIsSpark;
void main() {
vec4 viewPos4 = view * vec4(aPos, 1.0);
float dist = -viewPos4.z;
float scale = aIsSpark > 0.5 ? 0.12 : 0.3;
gl_PointSize = clamp(aSize * scale * push.screenHeight / max(dist, 1.0), 1.0, 64.0);
vLifeRatio = aLifeRatio;
vIsSpark = aIsSpark;
gl_Position = projection * viewPos4;
}

Binary file not shown.

View file

@ -0,0 +1,68 @@
#version 450
layout(set = 0, binding = 0) uniform sampler2D uComposite;
layout(push_constant) uniform Push {
vec4 rect;
vec2 playerUV;
float rotation;
float arrowRotation;
float zoomRadius;
int squareShape;
float opacity;
} push;
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
float cross2d(vec2 a, vec2 b) {
return a.x * b.y - a.y * b.x;
}
bool pointInTriangle(vec2 p, vec2 a, vec2 b, vec2 c) {
float d1 = cross2d(b - a, p - a);
float d2 = cross2d(c - b, p - b);
float d3 = cross2d(a - c, p - c);
bool hasNeg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
bool hasPos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
return !(hasNeg && hasPos);
}
void main() {
vec2 center = TexCoord - 0.5;
float dist = length(center);
if (push.squareShape == 0) {
if (dist > 0.5) discard;
}
float cs = cos(push.rotation);
float sn = sin(push.rotation);
vec2 rotated = vec2(center.x * cs - center.y * sn, center.x * sn + center.y * cs);
vec2 mapUV = push.playerUV + rotated * push.zoomRadius * 2.0;
vec4 mapColor = texture(uComposite, mapUV);
// Player arrow
float acs = cos(push.arrowRotation);
float asn = sin(push.arrowRotation);
vec2 ac = center;
vec2 arrowPos = vec2(ac.x * acs - ac.y * asn, ac.x * asn + ac.y * acs);
vec2 tip = vec2(0.0, -0.04);
vec2 left = vec2(-0.02, 0.02);
vec2 right = vec2(0.02, 0.02);
if (pointInTriangle(arrowPos, tip, left, right)) {
mapColor = vec4(1.0, 0.8, 0.0, 1.0);
}
// Dark border ring
float border = smoothstep(0.48, 0.5, dist);
if (push.squareShape == 0) {
mapColor.rgb *= 1.0 - border * 0.7;
}
outColor = vec4(mapColor.rgb, mapColor.a * push.opacity);
}

Binary file not shown.

View file

@ -0,0 +1,16 @@
#version 450
layout(push_constant) uniform Push {
vec4 rect; // x, y, w, h in 0..1 screen space
} push;
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 0) out vec2 TexCoord;
void main() {
TexCoord = aUV;
vec2 screenPos = push.rect.xy + aPos * push.rect.zw;
gl_Position = vec4(screenPos * 2.0 - 1.0, 0.0, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,11 @@
#version 450
layout(set = 0, binding = 0) uniform sampler2D uTileTexture;
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(uTileTexture, vec2(TexCoord.y, TexCoord.x));
}

Binary file not shown.

View file

@ -0,0 +1,17 @@
#version 450
layout(push_constant) uniform Push {
vec2 gridOffset;
} push;
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 0) out vec2 TexCoord;
void main() {
TexCoord = aUV;
vec2 pos = (aPos + push.gridOffset) / 3.0;
pos = pos * 2.0 - 1.0;
gl_Position = vec4(pos, 0.0, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,13 @@
#version 450
layout(location = 0) in float vAlpha;
layout(location = 0) out vec4 outColor;
void main() {
vec2 p = gl_PointCoord - vec2(0.5);
float dist = length(p);
if (dist > 0.5) discard;
float alpha = smoothstep(0.5, 0.1, dist) * vAlpha * 0.4;
outColor = vec4(0.7, 0.65, 0.55, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,26 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in float aSize;
layout(location = 2) in float aAlpha;
layout(location = 0) out float vAlpha;
void main() {
gl_PointSize = aSize;
vAlpha = aAlpha;
gl_Position = projection * view * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,14 @@
#version 450
// Full-screen color overlay (e.g. underwater tint).
// Uses postprocess.vert.glsl as vertex shader (fullscreen triangle, no vertex input).
layout(push_constant) uniform Push {
vec4 color; // rgb = tint color, a = opacity
} push;
layout(location = 0) out vec4 outColor;
void main() {
outColor = push.color;
}

Binary file not shown.

View file

@ -0,0 +1,20 @@
#version 450
layout(set = 0, binding = 0) uniform sampler2D uScene;
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
void main() {
vec3 hdr = texture(uScene, TexCoord).rgb;
// Shoulder tone map
vec3 mapped = hdr;
for (int i = 0; i < 3; i++) {
if (mapped[i] > 0.9) {
float excess = mapped[i] - 0.9;
mapped[i] = 0.9 + 0.1 * excess / (excess + 0.1);
}
}
outColor = vec4(mapped, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,10 @@
#version 450
layout(location = 0) out vec2 TexCoord;
void main() {
// Fullscreen triangle trick: 3 vertices, no vertex buffer
TexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(TexCoord * 2.0 - 1.0, 0.0, 1.0);
TexCoord.y = 1.0 - TexCoord.y; // flip Y for Vulkan
}

Binary file not shown.

View file

@ -0,0 +1,18 @@
#version 450
layout(set = 1, binding = 0) uniform sampler2D markerTexture;
layout(push_constant) uniform Push {
mat4 model;
float alpha;
} push;
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
void main() {
vec4 texColor = texture(markerTexture, TexCoord);
if (texColor.a < 0.1) discard;
outColor = vec4(texColor.rgb, texColor.a * push.alpha);
}

Binary file not shown.

View file

@ -0,0 +1,28 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
mat4 model;
} push;
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
layout(location = 0) out vec2 TexCoord;
void main() {
TexCoord = aTexCoord;
gl_Position = projection * view * push.model * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,19 @@
#version 450
layout(push_constant) uniform Push {
mat4 mvp;
vec4 color;
} push;
layout(location = 0) in vec2 vLocalPos;
layout(location = 0) out vec4 outColor;
void main() {
float r = length(vLocalPos);
float ring = smoothstep(0.93, 0.97, r) * smoothstep(1.0, 0.97, r);
float inward = (1.0 - smoothstep(0.0, 0.93, r)) * 0.15;
float alpha = max(ring, inward);
if (alpha < 0.01) discard;
outColor = vec4(push.color.rgb, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,14 @@
#version 450
layout(push_constant) uniform Push {
mat4 mvp;
} push;
layout(location = 0) in vec3 aPos;
layout(location = 0) out vec2 vLocalPos;
void main() {
vLocalPos = aPos.xz;
gl_Position = push.mvp * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,22 @@
#version 450
layout(set = 0, binding = 0) uniform sampler2D uTexture;
layout(set = 0, binding = 1) uniform ShadowParams {
int useBones;
int useTexture;
int alphaTest;
int foliageSway;
float windTime;
float foliageMotionDamp;
};
layout(location = 0) in vec2 TexCoord;
layout(location = 1) in vec3 WorldPos;
void main() {
if (useTexture != 0) {
vec4 texColor = textureLod(uTexture, TexCoord, 0.0);
if (alphaTest != 0 && texColor.a < 0.5) discard;
}
}

Binary file not shown.

View file

@ -0,0 +1,57 @@
#version 450
layout(push_constant) uniform Push {
mat4 lightSpaceMatrix;
mat4 model;
} push;
layout(set = 0, binding = 1) uniform ShadowParams {
int useBones;
int useTexture;
int alphaTest;
int foliageSway;
float windTime;
float foliageMotionDamp;
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
layout(location = 2) in vec4 aBoneWeights;
layout(location = 3) in vec4 aBoneIndicesF;
layout(location = 0) out vec2 TexCoord;
layout(location = 1) out vec3 WorldPos;
void main() {
vec4 pos = vec4(aPos, 1.0);
// Wind vertex displacement for foliage (matches m2.vert.glsl)
if (foliageSway != 0) {
vec3 worldRef = push.model[3].xyz;
float heightFactor = clamp(pos.z / 20.0, 0.0, 1.0);
heightFactor *= heightFactor;
// Layer 1: Trunk sway
float trunkPhase = windTime * 0.8 + dot(worldRef.xy, vec2(0.1, 0.13));
float trunkSwayX = sin(trunkPhase) * 0.35 * heightFactor;
float trunkSwayY = cos(trunkPhase * 0.7) * 0.25 * heightFactor;
// Layer 2: Branch sway
float branchPhase = windTime * 1.7 + dot(worldRef.xy, vec2(0.37, 0.71));
float branchSwayX = sin(branchPhase + pos.y * 0.4) * 0.15 * heightFactor;
float branchSwayY = cos(branchPhase * 1.1 + pos.x * 0.3) * 0.12 * heightFactor;
// Layer 3: Leaf flutter
float leafPhase = windTime * 4.5 + dot(aPos, vec3(1.7, 2.3, 0.9));
float leafFlutterX = sin(leafPhase) * 0.06 * heightFactor;
float leafFlutterY = cos(leafPhase * 1.3) * 0.05 * heightFactor;
pos.x += trunkSwayX + branchSwayX + leafFlutterX;
pos.y += trunkSwayY + branchSwayY + leafFlutterY;
}
vec4 worldPos = push.model * pos;
WorldPos = worldPos.xyz;
TexCoord = aTexCoord;
gl_Position = push.lightSpaceMatrix * worldPos;
}

Binary file not shown.

View file

@ -0,0 +1,97 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
vec4 zenithColor; // DBC skyTopColor
vec4 midColor; // DBC skyMiddleColor
vec4 horizonColor; // DBC skyBand1Color
vec4 fogColorPush; // DBC skyBand2Color
vec4 sunDirAndTime; // xyz = sun direction, w = timeOfDay
} push;
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
void main() {
// Reconstruct world-space ray direction from screen position.
float ndcX = TexCoord.x * 2.0 - 1.0;
float ndcY = -(TexCoord.y * 2.0 - 1.0);
vec3 viewDir = vec3(ndcX / projection[0][0],
ndcY / abs(projection[1][1]),
-1.0);
mat3 invViewRot = transpose(mat3(view));
vec3 worldDir = normalize(invViewRot * viewDir);
vec3 sunDir = push.sunDirAndTime.xyz;
float timeOfDay = push.sunDirAndTime.w;
// Elevation: +1 = zenith, 0 = horizon, -1 = nadir
float elev = worldDir.z;
float elevClamped = clamp(elev, 0.0, 1.0);
// --- 3-band sky gradient using DBC colors ---
// Zenith dominates upper sky, mid color fills the middle,
// horizon band at the bottom with a thin fog fringe.
vec3 sky;
if (elevClamped > 0.4) {
// Upper sky: mid -> zenith
float t = (elevClamped - 0.4) / 0.6;
sky = mix(push.midColor.rgb, push.zenithColor.rgb, t);
} else if (elevClamped > 0.05) {
// Lower sky: horizon -> mid (wide band)
float t = (elevClamped - 0.05) / 0.35;
sky = mix(push.horizonColor.rgb, push.midColor.rgb, t);
} else {
// Thin fog fringe right at horizon
float t = elevClamped / 0.05;
sky = mix(push.fogColorPush.rgb, push.horizonColor.rgb, t);
}
// --- Below-horizon darkening (nadir) ---
if (elev < 0.0) {
float nadirFade = clamp(-elev * 3.0, 0.0, 1.0);
vec3 nadirColor = push.fogColorPush.rgb * 0.3;
sky = mix(push.fogColorPush.rgb, nadirColor, nadirFade);
}
// --- Rayleigh-like scattering (subtle warm glow near sun) ---
float sunDot = max(dot(worldDir, sunDir), 0.0);
float sunAboveHorizon = clamp(sunDir.z, 0.0, 1.0);
float rayleighStrength = pow(1.0 - elevClamped, 3.0) * 0.15;
vec3 scatterColor = mix(vec3(0.8, 0.45, 0.15), vec3(0.3, 0.5, 1.0), elevClamped);
sky += scatterColor * rayleighStrength * sunDot * sunAboveHorizon;
// --- Mie-like forward scatter (sun disk glow) ---
float mieSharp = pow(sunDot, 64.0) * 0.4;
float mieSoft = pow(sunDot, 8.0) * 0.1;
vec3 sunGlowColor = mix(vec3(1.0, 0.85, 0.55), vec3(1.0, 1.0, 0.95), elevClamped);
sky += sunGlowColor * (mieSharp + mieSoft) * sunAboveHorizon;
// --- Subtle horizon haze ---
float hazeDensity = exp(-elevClamped * 12.0) * 0.06;
sky += push.horizonColor.rgb * hazeDensity * sunAboveHorizon;
// --- Night: slight moonlight tint ---
if (sunDir.z < 0.0) {
float moonlight = clamp(-sunDir.z * 0.5, 0.0, 0.15);
sky += vec3(0.02, 0.03, 0.08) * moonlight;
}
outColor = vec4(sky, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,12 @@
#version 450
// Fullscreen triangle sky — no vertex buffer, no mesh.
// Draws 3 vertices covering the entire screen, depth forced to 1.0 (far plane).
layout(location = 0) out vec2 TexCoord;
void main() {
// Produces triangle covering NDC [-1,1]² with depth = 1.0 (far)
TexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(TexCoord * 2.0 - 1.0, 1.0, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,13 @@
#version 450
layout(location = 0) in float vBrightness;
layout(location = 0) out vec4 outColor;
void main() {
vec2 p = gl_PointCoord - vec2(0.5);
float dist = length(p);
if (dist > 0.5) discard;
float alpha = vBrightness * smoothstep(0.5, 0.2, dist);
outColor = vec4(vec3(0.9, 0.95, 1.0) * vBrightness, alpha);
}

Binary file not shown.

View file

@ -0,0 +1,33 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
float time;
float intensity;
} push;
layout(location = 0) in vec3 aPos;
layout(location = 1) in float aBrightness;
layout(location = 2) in float aTwinklePhase;
layout(location = 0) out float vBrightness;
void main() {
mat4 rotView = mat4(mat3(view));
float twinkle = 0.7 + 0.3 * sin(push.time * 1.5 + aTwinklePhase);
vBrightness = aBrightness * twinkle * push.intensity;
gl_PointSize = mix(2.0, 4.0, aBrightness);
gl_Position = projection * rotView * vec4(aPos, 1.0);
}

Binary file not shown.

View file

@ -0,0 +1,15 @@
#version 450
layout(location = 0) in float vAlpha;
layout(location = 0) out vec4 outColor;
void main() {
vec2 p = gl_PointCoord - vec2(0.5);
float dist = length(p);
if (dist > 0.5) discard;
float ring = smoothstep(0.5, 0.4, dist) - smoothstep(0.38, 0.28, dist);
float highlight = smoothstep(0.3, 0.1, length(p - vec2(-0.15, 0.15))) * 0.5;
float alpha = (ring + highlight) * vAlpha;
outColor = vec4(0.8, 0.9, 1.0, alpha);
}

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more