Kelsidavis-WoWee/assets/shaders/wmo.frag.glsl

256 lines
9.2 KiB
Text
Raw Normal View History

#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 WMOMaterial {
int hasTexture;
int alphaTest;
int unlit;
int isInterior;
float specularIntensity;
int isWindow;
int enableNormalMap;
int enablePOM;
float pomScale;
int pomMaxSamples;
float heightMapVariance;
float normalMapStrength;
int isLava;
float wmoAmbientR;
float wmoAmbientG;
float wmoAmbientB;
};
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 vec4 VertColor;
layout(location = 4) in vec3 Tangent;
layout(location = 5) 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));
// Low density = close/head-on = full detail (0)
// High density = far/steep = vertex normals only (1)
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); // 1=head-on, 0=grazing
// Fade out POM at grazing angles to avoid distortion
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;
// Direction to shift UV per layer — clamp denominator to prevent explosion at grazing angles
vec2 P = viewDirTS.xy / max(VdotN, 0.15) * pomScale;
// Hard-clamp total UV offset to prevent texture swimming
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;
// Ray march through layers
for (int i = 0; i < 64; i++) {
if (i >= numSamples || currentLayerDepth >= currentDepthMapValue) break;
currentUV -= deltaUV;
currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
currentLayerDepth += layerDepth;
}
// Interpolate between last two layers for smooth result
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);
// Fade toward original UV at grazing angles for smooth transition
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;
// Compute final UV (with POM if enabled)
vec2 finalUV = TexCoord;
// Lava/magma: scroll UVs for flowing effect
if (isLava != 0) {
float time = fogParams.z;
// Scroll both axes — pools get horizontal flow, waterfalls get vertical flow
// (UV orientation depends on mesh, so animate both)
finalUV += vec2(time * 0.04, time * 0.06);
}
// 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 = hasTexture != 0 ? texture(uTexture, finalUV) : vec4(1.0);
if (alphaTest != 0 && texColor.a < 0.5) 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 = normalize(mapNormal);
vec3 worldNormal = normalize(TBN * mapNormal);
if (!gl_FrontFacing) worldNormal = -worldNormal;
// Linear blend: strength controls how much normal map detail shows,
// LOD fades out at distance. Both multiply for smooth falloff.
float blend = clamp(normalMapStrength, 0.0, 1.0) * (1.0 - lodFactor);
norm = normalize(mix(vertexNormal, worldNormal, blend));
}
vec3 result;
// Sample shadow map for all groups. Interior groups receive attenuated
// shadow (30%) so they get subtle light/shadow variation without the full
// outdoor darkening that makes them look wrong.
float shadow = 1.0;
if (shadowParams.x > 0.5) {
vec3 ldir = normalize(-lightDir.xyz);
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) {
feat(animation): 452 named constants, 30-phase character animation state machine Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND, anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1) lookup, and flyVariant() compact 218-element ground→FLY_* resolver. Expand AnimationController into a full state machine with 20+ named states: spell cast (directed→omni→cast fallback chain, instant one-shot release), hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle, stealth animation substitution, loot, fishing channel, sit/sleep/kneel down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons (BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY, vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS), emote state variants, off-hand/pierce/dual-wield alternation, NPC birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell. Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo (12 constants) namespaces; replace all magic numbers in spell_handler.cpp. Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire GameObjectStateCallback. Add DBC cross-validation on world entry. Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and asset_pipeline_gui.py. Add tests/test_animation_ids.cpp. Bug fixes included: - Stand state 1 was animating READY_2H(27) — fixed to SITTING(97) - Spell casts ended freeze-frame — add one-shot release animation - NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff) - Chair sits (states 2/4/5/6) incorrectly played floor-sit transition - STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
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);
}
if (isLava != 0) {
// Lava is self-luminous — bright emissive, no shadows
result = texColor.rgb * 1.5;
} else if (isInterior != 0) {
// WMO interior: vertex colors (MOCV) are pre-baked lighting from the artist.
// The MOHD ambient color floors the vertex colors so dark spots don't go
// completely black. Full shadow strength is applied but clamped so
// interiors never go darker than a minimum brightness.
vec3 wmoAmbient = vec3(wmoAmbientR, wmoAmbientG, wmoAmbientB);
wmoAmbient = max(wmoAmbient, vec3(0.35));
vec3 mocv = max(VertColor.rgb, wmoAmbient);
float clampedShadow = max(shadow, 0.45);
result = texColor.rgb * mocv * clampedShadow;
feat(rendering): add HiZ occlusion culling & fix WMO interior shadows Implement GPU-driven Hierarchical-Z occlusion culling for M2 doodads using a depth pyramid built from the previous frame's depth buffer. The cull shader projects bounding spheres via prevViewProj (temporal reprojection) and samples the HiZ pyramid to reject hidden objects before the main render pass. Key implementation details: - Separate early compute submission (beginSingleTimeCommands + fence wait) eliminates 2-frame visibility staleness - Conservative safeguards prevent false culls: screen-edge guard, full VP row-vector AABB projection (Cauchy-Schwarz), 50% sphere inflation, depth bias, mip+1, min screen size threshold, camera motion dampening (auto-disable on fast rotations), and per-instance previouslyVisible flag tracking - Graceful fallback to frustum-only culling if HiZ init fails Fix dark WMO interiors by gating shadow map sampling on isInterior==0 in the WMO fragment shader. Interior groups (flag 0x2000) now rely solely on pre-baked MOCV vertex-color lighting + MOHD ambient color. Disable interiorDarken globally (was incorrectly darkening outdoor M2s when camera was inside a WMO). Use isInsideInteriorWMO() instead of isInsideWMO() for correct indoor detection. New files: - hiz_system.hpp/cpp: pyramid image management, compute pipeline, descriptors, mip-chain build dispatch, resize handling - hiz_build.comp.glsl: MAX-depth 2x2 reduction compute shader - m2_cull_hiz.comp.glsl: frustum + HiZ occlusion cull compute shader - test_indoor_shadows.cpp: 14 unit tests for shadow/interior contracts Modified: - CullUniformsGPU expanded 128->272 bytes (HiZ params, viewProj, prevViewProj) - Depth buffer images gain VK_IMAGE_USAGE_SAMPLED_BIT for HiZ reads - wmo.frag.glsl: interior branch before unlit, shadow skip for 0x2000 - Render graph: hiz_build + compute_cull disabled (run in early compute) - .gitignore: ignore compiled .spv binaries - MEGA_BONE_MAX_INSTANCES: 2048 -> 4096 Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-06 16:40:59 +03:00
} else if (unlit != 0) {
// Outdoor unlit surface — still receives directional shadows
result = texColor.rgb * shadow;
} 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;
result = ambientColor.rgb * texColor.rgb
+ shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb);
result *= max(VertColor.rgb, vec3(0.5));
}
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 alpha = texColor.a;
// Window glass: opaque but simulates dark tinted glass with reflections.
if (isWindow != 0) {
vec3 viewDir = normalize(viewPos.xyz - FragPos);
float NdotV = abs(dot(norm, viewDir));
float fresnel = 0.08 + 0.92 * pow(1.0 - NdotV, 4.0);
vec3 ldir = normalize(-lightDir.xyz);
vec3 reflectDir = reflect(-viewDir, norm);
float sunGlint = pow(max(dot(reflectDir, ldir), 0.0), 32.0);
float baseBrightness = mix(0.3, 0.9, sunGlint);
vec3 glass = result * baseBrightness;
vec3 reflectTint = mix(ambientColor.rgb * 1.2, vec3(0.6, 0.75, 1.0), 0.6);
glass = mix(glass, reflectTint, fresnel * 0.8);
vec3 halfDir = normalize(ldir + viewDir);
float spec = pow(max(dot(norm, halfDir), 0.0), 256.0);
glass += spec * lightColor.rgb * 0.8;
float specBroad = pow(max(dot(norm, halfDir), 0.0), 12.0);
glass += specBroad * lightColor.rgb * 0.12;
result = glass;
if (isWindow == 2) {
// Instance/dungeon glass: mostly transparent to see through
alpha = mix(0.12, 0.35, fresnel);
} else {
alpha = mix(0.4, 0.95, NdotV);
}
}
outColor = vec4(result, alpha);
}