Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions code/def_files/data/effects/lensflare-f.sdr
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Physically-based lens flare (Lee & Eisemann 2013 matrix method).
// Each ghost is the image of the iris (aperture texture) clipped by the image
// of the circular entrance pupil, evaluated per color channel for chromatic
// fringing. The starburst instance samples the precomputed FFT texture instead.

struct lens_flare_instance {
vec4 center;
vec4 halfext;
vec4 apscale;
vec4 apoff;
vec4 color;
};

// Which artifact this quad draws. Mirrored from LENS_QUAD_* in
// graphics/util/uniform_structs.h, which also tabulates what each kind reads out
// of the fields above.
#define LENS_QUAD_GHOST 0.0
#define LENS_QUAD_STARBURST 1.0
#define LENS_QUAD_STREAK 2.0

// The tag travels as a float, so match it with a half-step tolerance rather than
// by equality.
bool quad_is(float tag, float kind) { return abs(tag - kind) < 0.5; }

#ifdef VULKAN
layout(location = 0) in vec2 sensorPos;
layout(location = 1) flat in vec4 g_center;
layout(location = 2) flat in vec4 g_halfext;
layout(location = 3) flat in vec4 g_apscale;
layout(location = 4) flat in vec4 g_apoff;
layout(location = 5) flat in vec4 g_color;
layout(location = 6) flat in float g_origin;

layout(location = 0) out vec4 fragOut0;

layout(set = 1, binding = 1) uniform sampler2D textures[16];
#define apertureMap textures[0]
#define starburstMap textures[1]
#else
in vec2 sensorPos;
flat in vec4 g_center;
flat in vec4 g_halfext;
flat in vec4 g_apscale;
flat in vec4 g_apoff;
flat in vec4 g_color;
flat in float g_origin;

out vec4 fragOut0;

uniform sampler2D apertureMap;
uniform sampler2D starburstMap;
#endif

#ifdef VULKAN
layout(std140, set = 2, binding = 0)
#else
layout(std140)
#endif
uniform genericData {
vec2 axis;
vec2 ndc_scale;
vec4 tint;
int n_instances;
float squeeze; // anamorphic horizontal stretch, 1.0 = spherical
// keep the array size in sync with MAX_LENS_FLARE_INSTANCES (uniform_structs.h)
lens_flare_instance instances[64];
};

// Undo the anamorphic stretch the vertex shader applied to this quad, so
// everything below stays in the rotationally-symmetric frame the optics were
// solved in. Both shaders stretch about the instance's axial centre, which the
// vertex shader hands over in g_origin -- they have to agree on that origin, or
// off-axis ghosts shear instead of stretching. At squeeze == 1.0 this is exactly
// the identity, so a spherical lens renders bit-for-bit as it did before.
vec2 unsqueeze(vec2 p)
{
vec2 d = p - axis * g_origin;
d.x /= squeeze;
return axis * g_origin + d;
}

// The anamorphic streak, generated rather than sampled: it is a smooth 1D
// profile, so a texture would cost an upload and a sampler binding to store a
// curve that two lines of arithmetic describe exactly.
//
// `d` is the offset from the sun's image, `halfext` the half-length (x) and
// half-thickness (y) of the bar. The bar tapers toward the tips instead of
// keeping a constant width, because a streak of even thickness reads as a drawn
// line rather than a lens artifact; the taper is floored so the tip never
// narrows past the point where it would alias.
float streak_profile(vec2 d, vec2 halfext)
{
float u = abs(d.x) / halfext.x; // 0 at the sun, 1 at the tip
if (u >= 1.0) {
return 0.0;
}

float taper = 1.0 - u;
float v = d.y / (halfext.y * max(taper, 0.15));
float across = exp(-v * v * 4.0);
// an even falloff down the length, plus a hot core so the streak visibly
// has a source rather than floating over the sun
float along = taper * taper + exp(-u * 12.0);
return across * along;
}

float ghost_channel(vec2 sp, float center, float halfext, float apscale, float apoff, float intensity)
{
vec2 q = (sp - axis * center) / halfext;
// image of the circular entrance pupil clips the bundle
float pupil = clamp((1.0 - length(q)) * 8.0, 0.0, 1.0);
vec2 uv = q * apscale + axis * apoff;
return texture(apertureMap, uv * 0.5 + 0.5).r * pupil * intensity;
}

void main()
{
vec3 result;

if (quad_is(g_center.w, LENS_QUAD_STREAK)) {
// The streak is laid out in sensor space by the vertex shader, so unlike
// the ghosts it must not be un-squeezed on the way back. halfext.xy is a
// half-length and a half-thickness here, not a chromatic triple.
result = streak_profile(sensorPos - axis * g_center.x, g_halfext.xy) * g_color.rgb;
} else {
vec2 sp = unsqueeze(sensorPos);
if (quad_is(g_center.w, LENS_QUAD_STARBURST)) {
// starburst billboard centered on the sun
vec2 q = (sp - axis * g_center.x) / g_halfext.x;
result = texture(starburstMap, q * 0.5 + 0.5).rgb * g_color.rgb;
} else {
result.r = ghost_channel(sp, g_center.x, g_halfext.x, g_apscale.x, g_apoff.x, g_color.x);
result.g = ghost_channel(sp, g_center.y, g_halfext.y, g_apscale.y, g_apoff.y, g_color.y);
result.b = ghost_channel(sp, g_center.z, g_halfext.z, g_apscale.z, g_apoff.z, g_color.z);
}
}

fragOut0 = vec4(result * tint.rgb, 1.0);
}
123 changes: 123 additions & 0 deletions code/def_files/data/effects/lensflare-v.sdr
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Physically-based lens flare (Lee & Eisemann 2013 matrix method).
// Instanced draw: one 4-vertex triangle-strip quad per ghost, plus one for the
// starburst billboard. All placement math was precomputed on the CPU into the
// per-instance data below; positions are in sensor-plane millimeters.

struct lens_flare_instance {
vec4 center; // w = LENS_QUAD_*, the kind tag; xyz meaning depends on it
vec4 halfext;
vec4 apscale;
vec4 apoff;
vec4 color;
};

// Which artifact this slot draws. Mirrored from LENS_QUAD_* in
// graphics/util/uniform_structs.h, which also tabulates what each kind reads out
// of the fields above.
#define LENS_QUAD_GHOST 0.0
#define LENS_QUAD_STARBURST 1.0
#define LENS_QUAD_STREAK 2.0

// The tag travels as a float, so match it with a half-step tolerance rather than
// by equality.
bool quad_is(float tag, float kind) { return abs(tag - kind) < 0.5; }

#ifdef VULKAN
layout(location = 0) out vec2 sensorPos;
layout(location = 1) flat out vec4 g_center;
layout(location = 2) flat out vec4 g_halfext;
layout(location = 3) flat out vec4 g_apscale;
layout(location = 4) flat out vec4 g_apoff;
layout(location = 5) flat out vec4 g_color;
layout(location = 6) flat out float g_origin;
#define INSTANCE_INDEX gl_InstanceIndex
#else
in vec4 vertPosition;
out vec2 sensorPos;
flat out vec4 g_center;
flat out vec4 g_halfext;
flat out vec4 g_apscale;
flat out vec4 g_apoff;
flat out vec4 g_color;
flat out float g_origin;
#define INSTANCE_INDEX gl_InstanceID
#endif

#ifdef VULKAN
layout(std140, set = 2, binding = 0)
#else
layout(std140)
#endif
uniform genericData {
vec2 axis; // unit flare axis in sensor space
vec2 ndc_scale; // sensor mm -> NDC
vec4 tint; // sun color * visibility * lens intensity
int n_instances;
float squeeze; // anamorphic horizontal stretch, 1.0 = spherical
// keep the array size in sync with MAX_LENS_FLARE_INSTANCES (uniform_structs.h)
lens_flare_instance instances[64];
};

void main()
{
#ifdef VULKAN
vec2 corner = vec2(float(gl_VertexIndex & 1), float((gl_VertexIndex >> 1) & 1)) * 2.0 - 1.0;
#else
vec2 corner = vertPosition.xy;
#endif

lens_flare_instance inst = instances[INSTANCE_INDEX];

vec2 p;
float origin;

if (quad_is(inst.center.w, LENS_QUAD_STREAK)) {
// Anamorphic streak: a screen-horizontal bar centred on the sun's image,
// built straight in sensor space rather than in the axis/perp frame. The
// streak lies along the cylindrical element, not along the line to the
// frame centre, so it stays horizontal wherever the sun sits. It is also
// exempt from `squeeze` -- its length is an explicit control, and
// stretching it as well would count the anamorphic twice.
// The vertical bound is padded well past the half-thickness: the gaussian
// across the bar is still ~2% of peak at one half-thickness, so a quad
// cut exactly there would leave a hard horizontal line down the whole
// streak. At 3x the profile has decayed to exp(-36), i.e. nothing. This
// only pads the geometry -- halfext.y still means half-thickness, so
// +Thickness: keeps its meaning.
origin = inst.center.x;
p = axis * origin + vec2(corner.x * inst.halfext.x, corner.y * inst.halfext.y * 3.0);
} else {
// Ghosts and the starburst share this path: both are laid out per channel
// in the axis/perp frame, the starburst simply with all three channels
// equal.
// Bounds of the union of the three chromatic quads, along/around the axis
float cmin = min(inst.center.x - inst.halfext.x, min(inst.center.y - inst.halfext.y, inst.center.z - inst.halfext.z));
float cmax = max(inst.center.x + inst.halfext.x, max(inst.center.y + inst.halfext.y, inst.center.z + inst.halfext.z));
float caxis = 0.5 * (cmin + cmax);
float haxis = 0.5 * (cmax - cmin);
float hperp = max(inst.halfext.x, max(inst.halfext.y, inst.halfext.z));

// The anamorphic stretch is linear, so applying it to the corner offset
// takes the oriented rectangle to a parallelogram that still bounds the
// stretched footprint exactly -- no need to widen out to a screen-aligned
// box. The stretch is about the instance's axial centre, not the sensor
// origin, so ghosts stay put and only grow: on a desqueezed anamorphic
// frame the horizontal squeeze of capture cancels for positions but not
// for footprints.
vec2 perp = vec2(-axis.y, axis.x);
vec2 off = axis * (corner.x * haxis) + perp * (corner.y * hperp);
off.x *= squeeze;
origin = caxis;
p = axis * caxis + off;
}

sensorPos = p;
g_origin = origin;
g_center = inst.center;
g_halfext = inst.halfext;
g_apscale = inst.apscale;
g_apoff = inst.apoff;
g_color = inst.color;

gl_Position = vec4(p * ndc_scale, 0.0, 1.0);
}
Loading
Loading