Five things, all of them "this happens too fast or looks too thin". PACING. Every beat had a completion test that could be satisfied while the guide was still mid-sentence, which is why everything landed seconds after the thing before it. Beats now have a FLOOR as well as a deadline - a minimum length - and a beat whose point is the guide talking will not end while a line is still on screen. One rule in one place rather than tuning each condition. The run is 3400px instead of 1500, which at cruise was about four seconds, and a held line is 6.5s instead of 4.5. THE CONVOY. Escorts at the four corners of a box, holds packed in the middle of it, roughly three times the spread of the fleet's own wedge - this is freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION pair index-for-index, which is all it takes for spawn_ships to seat the guns on the outside, and the test pins that every hold sits further in than the furthest-out escort. THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip along the bottom and a caret in the corner - geometry rather than art, so it scales with the banner and costs nothing. THE BREACH. It was a few lines turning around a point. It is now two halves driven by one number: DimensionalBreach, a growing black hole out in the world built on BlackHoleVisual's idiom - empty core, counter-rotating arc segments hotter toward the centre - with infalling streamers it does not have, because a planet's disc is steady and this one has to say "now"; and breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships, rocks and the starfield all get dragged in, which is most of what makes a hole in space read as a hole rather than as a decal drawn on one. Inside the horizon the shader samples nothing at all - the same decision BlackHoleVisual makes, for the same reason: a black hole with something visible in the middle stops being one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
3.1 KiB
Text
72 lines
3.1 KiB
Text
shader_type canvas_item;
|
|
|
|
// #125: the dimensional breach, as a screen-space lens.
|
|
//
|
|
// The prologue's warp used to be a handful of lines turning around a point.
|
|
// This bends the ACTUAL SCENE around the breach instead - the ships, the
|
|
// rocks and the starfield all get dragged into it - which is most of what
|
|
// makes a hole in space read as a hole rather than as a decal drawn on top
|
|
// of one.
|
|
//
|
|
// Two effects, both radial and both fading to nothing at the edge of their
|
|
// reach, so the shader is invisible outside its own radius and costs a
|
|
// clamped texture fetch everywhere else:
|
|
//
|
|
// swirl angle offset that grows toward the centre, so the scene winds
|
|
// around the hole rather than merely sliding toward it
|
|
// pull inward displacement, strongest just outside the horizon, which
|
|
// is the lensing itself
|
|
//
|
|
// Inside the horizon nothing is sampled at all: the core is drawn as empty,
|
|
// the same decision BlackHoleVisual makes for the black-hole planet, and
|
|
// for the same reason - a "black hole" with something visible in the middle
|
|
// stops reading as one.
|
|
|
|
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_linear;
|
|
|
|
// Where the breach is, in SCREEN pixels, and how big it has grown.
|
|
uniform vec2 breach_center = vec2(360.0, 640.0);
|
|
uniform vec2 viewport_size = vec2(720.0, 1280.0);
|
|
// The event horizon: everything inside is void.
|
|
uniform float horizon_radius = 0.0;
|
|
// How far the lens reaches past the horizon. Beyond this the scene is
|
|
// untouched.
|
|
uniform float lens_radius = 1.0;
|
|
// 0 while the breach is opening, 1 at full strength. Scales both effects
|
|
// together so one knob fades the whole thing in.
|
|
uniform float strength = 0.0;
|
|
uniform float swirl_turns = 2.4;
|
|
uniform vec4 rim_color : source_color = vec4(0.72, 0.45, 1.0, 1.0);
|
|
|
|
void fragment() {
|
|
vec2 pixel = UV * viewport_size;
|
|
vec2 offset = pixel - breach_center;
|
|
float distance_to_centre = length(offset);
|
|
|
|
if (distance_to_centre >= lens_radius || strength <= 0.001) {
|
|
COLOR = texture(screen_texture, SCREEN_UV);
|
|
} else if (distance_to_centre <= horizon_radius) {
|
|
// The core. Not black-with-alpha: genuinely nothing, so whatever is
|
|
// behind the breach is gone rather than dimmed.
|
|
COLOR = vec4(0.0, 0.0, 0.0, 1.0);
|
|
} else {
|
|
// 1 at the horizon, 0 at the edge of the lens. Squared so the bend
|
|
// is concentrated near the hole instead of smeared evenly outward.
|
|
float falloff = 1.0 - clamp(
|
|
(distance_to_centre - horizon_radius) / max(lens_radius - horizon_radius, 1.0),
|
|
0.0, 1.0);
|
|
float bend = falloff * falloff * strength;
|
|
|
|
float angle = atan(offset.y, offset.x) + bend * swirl_turns;
|
|
float pulled = distance_to_centre - bend * (lens_radius - horizon_radius) * 0.55;
|
|
pulled = max(pulled, horizon_radius);
|
|
vec2 sampled = breach_center + vec2(cos(angle), sin(angle)) * pulled;
|
|
|
|
COLOR = texture(screen_texture, sampled / viewport_size);
|
|
|
|
// A hot rim where the lensing is fiercest, which is what gives the
|
|
// horizon an edge rather than a fade.
|
|
float rim = smoothstep(0.72, 1.0, falloff) * strength;
|
|
COLOR.rgb = mix(COLOR.rgb, rim_color.rgb, rim * 0.65);
|
|
}
|
|
}
|