Files
aego/engine/res/shaders/include/lighting.glsl
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

162 lines
7.7 KiB
GLSL

// Forward 2D: each receiver evaluates sources with its own surface normal.
// Normal images are linear, +Y image-up; source-local UV is image-down.
#if AEGO_FRAGMENT && defined(AEGO_LIGHTING)
vec3 aego_surface_normal(float strength) {
#ifdef AEGO_MESH_SURFACE
// Derivatives must be evaluated before per-fragment early returns: GLSL
// leaves them undefined in non-uniform control flow around trim edges.
vec2 dx = dFdx(aego_surface_local_position);
vec2 dy = dFdy(aego_surface_local_position);
vec2 ux = dFdx(aego_local);
vec2 uy = dFdy(aego_local);
#endif
vec2 span = aego_normal_local_rect.zw - aego_normal_local_rect.xy;
vec2 uvspan = aego_normal_region.zw - aego_normal_region.xy;
if (abs(span.x * span.y) < 1e-8 || abs(uvspan.x * uvspan.y) < 1e-12) {
return vec3(0.0, 0.0, 1.0);
}
vec2 local = (aego_local - aego_normal_local_rect.xy) / span;
if (any(lessThan(local,vec2(0.0))) || any(greaterThan(local,vec2(1.0)))) {
return vec3(0.0, 0.0, 1.0);
}
vec2 halfTexel = 0.5 / vec2(textureSize(aego_normal_map,0));
vec2 lo = min(aego_normal_region.xy,aego_normal_region.zw) + halfTexel;
vec2 hi = max(aego_normal_region.xy,aego_normal_region.zw) - halfTexel;
vec2 uv = clamp(mix(aego_normal_region.xy,aego_normal_region.zw,local),lo,hi);
vec3 n = texture(aego_normal_map,uv).xyz * 2.0 - 1.0;
n.xy *= max(strength,0.0);
if (dot(n,n) < 1e-10) { n = vec3(0.0,0.0,1.0); }
n = normalize(n);
#ifdef AEGO_MESH_SURFACE
// Reconstruct the basis from actual geometry and UV. Degenerate UV has an
// explicit flat fallback, never a NaN or an arbitrary highlight direction.
float detUV = ux.x * uy.y - ux.y * uy.x;
if (abs(detUV) < 1e-10) { return vec3(0.0,0.0,1.0); }
vec2 tx = (dx * uy.y - dy * ux.y) / detUV;
vec2 ty = (dy * ux.x - dx * uy.x) / detUV;
if (dot(tx,tx)<1e-10 || dot(ty,ty)<1e-10) { return vec3(0.0,0.0,1.0); }
float orientation = tx.x*ty.y-tx.y*ty.x;
if (abs(orientation)<1e-10) { return vec3(0.0,0.0,1.0); }
tx = normalize(tx);
ty = vec2(-tx.y,tx.x)*sign(orientation);
mat2 basis = mat2(aego_surface_basis.xy,aego_surface_basis.zw)*mat2(tx,ty);
#else
mat2 basis = mat2(aego_surface_basis.xy,aego_surface_basis.zw);
// UV sampling is already reflected. Reflect vector orientation exactly
// once, independently from a reflected parent/node affine matrix.
n.xy *= sign(aego_local_rect.zw - aego_local_rect.xy);
#endif
if (abs(determinant(basis)) < 1e-8) { return vec3(0.0,0.0,1.0); }
vec2 xy = transpose(inverse(basis)) * vec2(n.x,-n.y);
return normalize(vec3(xy,n.z));
}
float aego_shadow_visibility(int i, vec2 position) {
float layer = aego_light_shape[i].z;
if (layer < 0.0) { return 1.0; }
vec2 uv = (position-aego_light_pose[i].xy)/(2.0*aego_light_color[i].w)+0.5;
if (uv.y < aego_light_cookie[i].y || any(lessThan(uv,vec2(0.0))) || any(greaterThan(uv,vec2(1.0)))) { return 1.0; }
float radius = aego_light_shape[i].w;
if (radius <= 0.0) { return texture(aego_shadow_masks,vec3(uv,layer)).r; }
// Limited mask filtering; all taps use the same array layer.
float visibility = 0.0;
for (int y=-1;y<=1;y++) {
for (int x=-1;x<=1;x++) {
visibility += texture(aego_shadow_masks,vec3(clamp(uv+vec2(float(x),float(y))*radius,vec2(0.0),vec2(1.0)),layer)).r;
}
}
return visibility/9.0;
}
// A typed call fixes only per-draw control flow. All light parameters, shadow
// and cookie samples, attenuation arithmetic and accumulation order stay live.
void aego_apply_light(int i, int lightType, bool shadowed, vec3 normal, bool receiveShadows, inout vec3 result) {
vec4 pose = aego_light_pose[i];
vec4 color = aego_light_color[i];
vec4 cone = aego_light_cone[i];
vec4 shape = aego_light_shape[i];
vec3 direction;
float attenuation = 1.0;
if (lightType==2) {
direction = vec3(-cone.xy*cos(shape.y),sin(shape.y));
} else {
vec2 delta = pose.xy-aego_surface_world.xy;
float distanceXY = length(delta);
if (distanceXY >= color.w) { return; }
attenuation = pow(max(0.0,1.0-distanceXY/color.w),shape.x);
vec3 toward = vec3(delta,pose.z);
float lengthSquared = dot(toward,toward);
direction = lengthSquared > 1e-12 ? toward*inversesqrt(lengthSquared) : vec3(0.0,0.0,1.0);
if (lightType==1) {
float cosine = distanceXY > 1e-6 ? dot(-delta/distanceXY,cone.xy) : 1.0;
attenuation *= cone.z > cone.w ? smoothstep(cone.w,cone.z,cosine) : step(cone.w,cosine);
}
if (lightType==3) {
float cookieLayer = aego_light_cookie[i].x;
if (cookieLayer < 0.0) { return; }
vec2 local = vec2(dot(-delta,cone.xy),dot(-delta,vec2(-cone.y,cone.x)))/(2.0*color.w)+0.5;
attenuation *= texture(aego_cookies,vec3(clamp(local,vec2(0.0),vec2(1.0)),cookieLayer)).r;
}
// The direct term is exactly zero behind a receiver or outside its
// spot/cookie. Skip the PCF reads for those fragments.
if (attenuation == 0.0 || dot(normal,direction) <= 0.0) { return; }
if (receiveShadows && shadowed) { attenuation *= aego_shadow_visibility(i,aego_surface_world.xy); }
}
result += color.rgb * (attenuation * max(0.0,dot(normal,direction)));
}
vec3 aego_lighting(vec3 normal, bool receiveShadows) {
vec3 result = aego_ambient_count.rgb;
#ifdef AEGO_TYPED_LIGHT_CALLS
AEGO_TYPED_LIGHT_CALLS
#else
for (int i=0;i<8;i++) {
if (i>=int(aego_ambient_count.w)) { break; }
aego_apply_light(i, int(aego_light_pose[i].w), true, normal, receiveShadows, result);
}
#endif
return result;
}
// Emission RGB is straight color; an emission mask is an independent scalar.
// The source canvas, trim and atlas rectangle belong to this channel, not to
// albedo. Coverage is applied exactly once by aego_lit, from the albedo alpha.
vec3 aego_emission(vec3 color, float intensity) {
vec3 value = color * max(intensity,0.0);
#ifdef AEGO_SURFACE_CHANNELS
int mode = int(aego_surface_channel_modes.x);
if (mode == 0) { return value; }
vec2 span = aego_emission_local_rect.zw-aego_emission_local_rect.xy;
vec2 uvspan = aego_emission_region.zw-aego_emission_region.xy;
if (abs(span.x*span.y)<1e-8 || abs(uvspan.x*uvspan.y)<1e-12) { return vec3(0.0); }
vec2 local = (aego_local-aego_emission_local_rect.xy)/span;
if (any(lessThan(local,vec2(0.0))) || any(greaterThan(local,vec2(1.0)))) { return vec3(0.0); }
vec2 halfTexel = 0.5/vec2(textureSize(aego_emission_map,0));
vec2 lo = min(aego_emission_region.xy,aego_emission_region.zw)+halfTexel;
vec2 hi = max(aego_emission_region.xy,aego_emission_region.zw)-halfTexel;
vec2 uv = clamp(mix(aego_emission_region.xy,aego_emission_region.zw,local),lo,hi);
vec3 sampleColor = texture(aego_emission_map,uv).rgb;
value *= mode == 2 ? vec3(sampleColor.r) : sampleColor;
#endif
return value;
}
vec4 aego_lit(vec4 premultipliedAlbedo, float normalStrength, vec3 emission, bool receiveShadows) {
vec3 normal = aego_surface_normal(normalStrength);
int debugMode = int(aego_light_cookie[0].w);
if (debugMode==3) { return premultipliedAlbedo; }
if (debugMode==1) { return vec4((normal*0.5+0.5)*premultipliedAlbedo.a,premultipliedAlbedo.a); }
if (debugMode==2) {
float visibility = 1.0;
for (int i=0;i<8;i++) {
if (i>=int(aego_ambient_count.w)) { break; }
visibility = min(visibility,aego_shadow_visibility(i,aego_surface_world.xy));
}
return vec4(vec3(visibility)*premultipliedAlbedo.a,premultipliedAlbedo.a);
}
vec3 rgb = premultipliedAlbedo.rgb*aego_lighting(normal,receiveShadows);
rgb += emission*premultipliedAlbedo.a;
return vec4(rgb,premultipliedAlbedo.a);
}
#endif