2026-10-05 19:58:18 +03:00

226 lines
8.9 KiB
GLSL

@vs vs_trile
@include_block view
layout(binding=1) uniform trile_params {
vec4 orientations[72];
float show_hidden; // shadow and reflection passes still draw cut triles
};
in vec3 position;
in vec4 data_n; // bytes / 255: x = swatch, y = ao (0..3), z = normal index
in vec4 instance; // xyz = position, w = orientation
in float tint;
in float cut;
out float v_clip;
out vec3 v_to_camera;
out vec3 v_world;
out vec3 v_normal;
out float v_swatch;
out float v_facing;
out float v_ao;
out vec3 v_shade;
out vec3 v_sun;
out float v_tint;
out float v_cut;
void main() {
vec4 data = data_n * 255.0;
int ori = int(instance.w + 0.5) * 3;
mat3 rotation = transpose(mat3(orientations[ori].xyz, orientations[ori + 1].xyz, orientations[ori + 2].xyz));
vec3 world = rotation * (position - 0.5) + 0.5 + instance.xyz;
int n = int(data.z + 0.5);
vec3 normal = vec3(0.0);
normal[n / 2] = (n % 2 == 0) ? 1.0 : -1.0;
v_normal = rotation * normal;
v_facing = smoothstep(0.0, 0.3, dot(v_normal, sun_direction.xyz));
v_ao = data.y / 3.0;
v_shade = shade_color.rgb;
v_sun = sun_color.rgb;
gl_Position = view_proj * vec4(world, 1.0);
v_cut = show_hidden > 0.5 ? 0.0 : cut;
// Fully cut triles are collapsed outside the clip volume, so nothing is drawn.
if (v_cut > 0.999) gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
v_to_camera = camera.xyz - world;
v_world = world;
v_clip = dot(vec4(world, 1.0), clip);
v_swatch = data.x;
v_tint = tint;
}
@end
@block trile_in
in float v_clip;
in vec3 v_to_camera;
in vec3 v_world;
in vec3 v_normal;
in float v_swatch;
in float v_facing;
in float v_ao;
in vec3 v_shade;
in vec3 v_sun;
in float v_tint;
in float v_cut;
@end
@block palette
layout(binding=0) uniform texture2D palette;
layout(binding=0) uniform sampler palette_smp;
@end
@fs fs_trile
@include_block scene_out
@include_block emissive
@include_block shadow
@include_block trile_in
@include_block palette
layout(binding=2) uniform trile_fs_params {
vec4 sun_dir; // towards the sun
vec4 probe_min; // xyz = first chunk in probe_index, w = 1 when there are probes
vec4 probe_size; // xyz = probe_index size in chunks
float use_reflection; // the reflection pass can't read its own image
float reflection_height;
};
layout(binding=1) uniform texture2D trile_reflection;
layout(binding=2) uniform texture2D trile_reflection_glow;
layout(binding=1) uniform sampler trile_reflection_smp;
layout(binding=3) uniform texture3D probe_index; // per chunk: brick number + 1 in r + g * 256, 0 = none
layout(binding=4) uniform texture3D probe_atlas; // per brick 32^3 probes, 3 texels each: red, green, blue
layout(binding=2) uniform sampler probe_smp;
const float GLOSS_GLOW = 4.0; // how much a full gloss highlight blooms
const float REFLECTED_GLOW_CURVE = 0.4; // below 1, reflected glow fades slower than reflected colour
const float GLASS_PROJECTION = 2.0; // how strongly coloured sunlight paints its colour on
const int PROBE_BRICKS_PER_COLUMN = 16;
// The probe at the centre of the trile at cell, unless there is none or it is inside geometry.
bool probe_at(ivec3 cell, out vec4 r, out vec4 g, out vec4 b) {
ivec3 chunk = ivec3(floor(vec3(cell) / 32.0));
ivec3 slot = chunk - ivec3(probe_min.xyz);
if (any(lessThan(slot, ivec3(0))) || any(greaterThanEqual(slot, ivec3(probe_size.xyz)))) return false;
vec2 number = texelFetch(sampler3D(probe_index, probe_smp), slot, 0).rg * 255.0 + 0.5;
int brick = int(number.x) + int(number.y) * 256 - 1;
if (brick < 0) return false;
ivec3 local = cell - chunk * 32;
ivec3 texel = ivec3(local.x * 3, (brick % PROBE_BRICKS_PER_COLUMN) * 32 + local.y, (brick / PROBE_BRICKS_PER_COLUMN) * 32 + local.z);
r = texelFetch(sampler3D(probe_atlas, probe_smp), texel, 0);
g = texelFetch(sampler3D(probe_atlas, probe_smp), texel + ivec3(1, 0, 0), 0);
b = texelFetch(sampler3D(probe_atlas, probe_smp), texel + ivec3(2, 0, 0), 0);
return r.x >= 0.0;
}
// Light from the eight probes around a point half a trile out from the surface,
// leaving out probes behind the surface or inside geometry. Like sun_shadow it is
// taken at the trixel face centre, so it steps per trixel. a = 0 when no probe counts.
vec4 probe_light(vec3 world, vec3 normal) {
vec3 half_trixel = normal * (0.5 / 16.0);
vec3 face = (floor((world - half_trixel) * 16.0) + 0.5) / 16.0 + half_trixel;
vec3 grid = face + normal * 0.5 - 0.5;
ivec3 base = ivec3(floor(grid));
vec3 f = grid - vec3(base);
vec4 sum_r = vec4(0.0), sum_g = vec4(0.0), sum_b = vec4(0.0);
float total = 0.0;
for (int i = 0; i < 8; i++) {
ivec3 offset = ivec3(i & 1, (i >> 1) & 1, i >> 2);
ivec3 cell = base + offset;
if (dot(vec3(cell) + 0.5 - face, normal) < 0.01) continue;
vec4 r, g, b;
if (!probe_at(cell, r, g, b)) continue;
vec3 t = mix(1.0 - f, f, vec3(offset));
float w = max(t.x * t.y * t.z, 0.0001);
sum_r += r * w;
sum_g += g * w;
sum_b += b * w;
total += w;
}
if (total == 0.0) return vec4(0.0);
// Irradiance over pi from the SH: the cosine lobe convolved, both bands.
vec4 lobe = vec4(0.282095, 0.325735 * normal.y, 0.325735 * normal.z, 0.325735 * normal.x);
return vec4(max(vec3(dot(sum_r, lobe), dot(sum_g, lobe), dot(sum_b, lobe)) / total, 0.0), 1.0);
}
// Ordered dither, so dissolving triles fade without blending.
float dither(vec2 pixel) {
const float bayer[16] = float[16](0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);
ivec2 p = ivec2(pixel) % 4;
return (bayer[p.y * 4 + p.x] + 0.5) / 16.0;
}
void main() {
if (v_clip < 0.0 || v_cut > dither(gl_FragCoord.xy)) discard;
int id = int(v_swatch + 0.5);
vec4 swatch = texelFetch(sampler2D(palette, palette_smp), ivec2(id, 0), 0);
vec4 material = texelFetch(sampler2D(palette, palette_smp), ivec2(id, 1), 0); // g = reflectivity, b = gloss
vec3 albedo = swatch.rgb;
if (v_tint > 1.5) albedo = mix(albedo, vec3(1.0, 0.2, 0.2), 0.5);
else if (v_tint > 0.5) albedo = mix(albedo, vec3(0.2, 1.0, 0.2), 0.5);
vec3 sun = v_facing * sun_shadow(v_world, v_normal);
vec3 lit = sun * mix(0.25, 1.0, v_ao);
vec4 probe = probe_min.w > 0.5 ? probe_light(v_world, v_normal) : vec4(0.0);
vec3 shade = v_shade + probe.rgb;
vec3 light = mix(shade * mix(0.6, 1.0, v_ao), v_sun, lit);
vec3 color = albedo * light;
// Light through glass paints the glass colour onto what it hits, so the pattern
// shows on dark surfaces too, not only where the albedo can reflect it.
float bright = brightest(sun);
if (bright > 0.001) {
float tinted = (bright - min(sun.r, min(sun.g, sun.b))) / bright;
color = mix(color, sun * v_sun, min(1.0, tinted * GLASS_PROJECTION));
}
float glow = swatch.a * EMISSIVE_GLOW;
// Glossier is both stronger and sharper.
if (material.b > 0.0) {
vec3 halfway = normalize(normalize(v_to_camera) + sun_dir.xyz);
float highlight = pow(max(dot(v_normal, halfway), 0.0), mix(4.0, 96.0, material.b)) * material.b;
color += v_sun * sun * highlight;
glow += highlight * brightest(sun) * GLOSS_GLOW;
}
// Only floors lying on the reflection plane can show the planar reflection.
if (use_reflection > 0.5 && material.g > 0.0 && v_normal.y > 0.5 && abs(v_world.y - reflection_height) < 0.02) {
vec2 screen = gl_FragCoord.xy / vec2(textureSize(sampler2D(trile_reflection, trile_reflection_smp), 0));
vec2 uv = vec2(screen.x, 1.0 - screen.y);
color = mix(color, texture(sampler2D(trile_reflection, trile_reflection_smp), uv).rgb, material.g);
glow = max(glow, texture(sampler2D(trile_reflection_glow, trile_reflection_smp), uv).r * pow(material.g, REFLECTED_GLOW_CURVE));
}
// Emissive swatches show their own colour whatever the lighting.
frag_color = vec4(mix(color, albedo, swatch.a), 1.0);
write_scene(v_to_camera, glow);
}
@end
@fs fs_trile_shadow
@include_block trile_in
@include_block palette
void main() {
// Glass lets the sun through.
if (texelFetch(sampler2D(palette, palette_smp), ivec2(int(v_swatch + 0.5), 1), 0).r > 0.5) discard;
}
@end
// Drawn after the opaque casters and multiplied into the shadow tint, so light
// through several panes takes all their colours.
@fs fs_trile_glass_shadow
@include_block trile_in
@include_block palette
out vec4 frag_color;
void main() {
int id = int(v_swatch + 0.5);
if (texelFetch(sampler2D(palette, palette_smp), ivec2(id, 1), 0).r < 0.5) discard;
vec3 c = texelFetch(sampler2D(palette, palette_smp), ivec2(id, 0), 0).rgb;
// Tinted rather than darkened: the glass's brightest channel passes fully.
frag_color = vec4(c / max(max(c.r, max(c.g, c.b)), 0.001), gl_FragCoord.z);
}
@end
@program trile vs_trile fs_trile
@program trile_shadow vs_trile fs_trile_shadow
@program trile_glass_shadow vs_trile fs_trile_glass_shadow