--- a/plugins/cube/cube.cpp +++ b/plugins/cube/cube.cpp @@ -724,6 +724,10 @@ ensure_warp_option("hyperspace_coloured_stars", 0.12); ensure_warp_option("hyperspace_distant_stars", 1.0); ensure_warp_option("hyperspace_distant_drift", 0.10); + ensure_warp_option("hyperspace_dust_density", 0.90); + ensure_warp_option("hyperspace_cloud_variation", 1.15); + ensure_warp_option("hyperspace_pulsars", 0.65); + ensure_warp_option("hyperspace_seed", 0); ensure_warp_option("hyperspace_color", wf::color_t{0.20, 0.52, 1.0, 1.0}); ensure_warp_option("hyperspace_quality", 1); ensure_warp_option("hyperspace_fps", 30); --- a/plugins/cube/hyperspace-background.hpp +++ b/plugins/cube/hyperspace-background.hpp @@ -3,10 +3,11 @@ #include #include #include +#include #include -// A self-contained procedural backdrop: reduced-resolution cloud shells, -// slowly drifting distant stars and full-resolution foreground trails. +// A self-contained procedural backdrop: layered nebulae, independent dust, +// distant stars and passing stars, with occasional distant pulsar jets. class __attribute__((visibility("hidden"))) wf_cube_hyperspace_background { wf::option_wrapper_t speed{"cube/hyperspace_speed"}; @@ -20,20 +21,31 @@ wf::option_wrapper_t coloured{"cube/hyperspace_coloured_stars"}; wf::option_wrapper_t distant_density{"cube/hyperspace_distant_stars"}; wf::option_wrapper_t distant_drift{"cube/hyperspace_distant_drift"}; + wf::option_wrapper_t dust_density{"cube/hyperspace_dust_density"}; + wf::option_wrapper_t variation{"cube/hyperspace_cloud_variation"}; + wf::option_wrapper_t pulsars{"cube/hyperspace_pulsars"}; + wf::option_wrapper_t seed_option{"cube/hyperspace_seed"}; wf::option_wrapper_t tint{"cube/hyperspace_color"}; wf::option_wrapper_t quality{"cube/hyperspace_quality"}; wf::option_wrapper_t fps{"cube/hyperspace_fps"}; - OpenGL::program_t cloud_program, star_program; + OpenGL::program_t cloud_program, star_program, pulsar_program; wf::auxilliary_buffer_t cloud_buffer, backdrop_buffer; GLuint star_buffer = 0; - int near_count = -1, far_count = -1; + int near_count = -1, far_count = -1, dust_count = -1; + int configured_seed = -1; + uint32_t scene_seed = 1, event_number = 0; + unsigned travel_epoch = 0; bool active = false, transition = false, cloud_dirty = true, backdrop_dirty = true; double travel = 0, cloud_phase = 0, drift_phase = 0, boost = 0; double rendered_speed = 0; + double pulsar_age = -3, pulsar_duration = 15; + float pulsar_x = 0.5f, pulsar_y = 0.3f, pulsar_angle = 0, pulsar_rotation = 0.2f; + float pulsar_size = 0.13f, pulsar_projection = 0.65f; std::chrono::steady_clock::time_point last_frame; void load_programs(); void update_particles(); + void prepare_pulsar(); public: explicit wf_cube_hyperspace_background(std::function changed); --- a/plugins/cube/hyperspace-background.cpp +++ b/plugins/cube/hyperspace-background.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace { @@ -32,6 +33,8 @@ ribbons.set_callback(update); glow.set_callback(update); brightness.set_callback(update); surge.set_callback(update); density.set_callback(update); coloured.set_callback(update); distant_density.set_callback(update); distant_drift.set_callback(update); + dust_density.set_callback(update); variation.set_callback(update); + pulsars.set_callback(update); seed_option.set_callback(update); tint.set_callback(update); quality.set_callback(update); fps.set_callback(update); last_frame = std::chrono::steady_clock::now(); } @@ -40,7 +43,7 @@ { wf::gles::run_in_context_if_gles([&] { - cloud_program.free_resources(); star_program.free_resources(); + cloud_program.free_resources(); star_program.free_resources(); pulsar_program.free_resources(); cloud_buffer.free(); backdrop_buffer.free(); if (star_buffer) { GL_CALL(glDeleteBuffers(1, &star_buffer)); } }); @@ -70,24 +73,54 @@ cloud_program.set_simple(OpenGL::compile_program(hyperspace_vertex, hyperspace_cloud_fragment)); if (!star_program.get_program_id(wf::TEXTURE_TYPE_RGBA)) star_program.set_simple(OpenGL::compile_program(hyperspace_star_vertex, hyperspace_star_fragment)); + if (!pulsar_program.get_program_id(wf::TEXTURE_TYPE_RGBA)) + pulsar_program.set_simple(OpenGL::compile_program(hyperspace_vertex, hyperspace_pulsar_fragment)); +} + +void wf_cube_hyperspace_background::prepare_pulsar() +{ + uint32_t base = scene_seed ^ (++event_number * 0x9e3779b9u); + pulsar_x = 0.12f + 0.76f * star_random(base); + pulsar_y = 0.15f + 0.70f * star_random(base + 1); + pulsar_angle = star_random(base + 2) * 2 * M_PI; + pulsar_rotation = (0.10f + 0.23f * star_random(base + 3)) * + (star_random(base + 4) < 0.5f ? -1 : 1); + pulsar_size = 0.065f + 0.095f * star_random(base + 5); + pulsar_projection = 0.45f + 0.45f * star_random(base + 6); + pulsar_duration = 10 + 10 * star_random(base + 7); } void wf_cube_hyperspace_background::update_particles() { + bool reseed = configured_seed != int(seed_option); + if (reseed) + { + configured_seed = int(seed_option); + scene_seed = configured_seed > 0 ? uint32_t(configured_seed) : std::random_device{}(); + near_count = far_count = dust_count = -1; + event_number = travel_epoch = 0; + prepare_pulsar(); pulsar_age = -3; + } const int near_counts[] = {768, 2048, 4096}, far_counts[] = {1024, 2560, 5120}; + const int dust_counts[] = {1800, 4500, 9000}; int level = std::clamp(int(quality), 0, 2); int near = int(near_counts[level] * std::clamp(double(density), 0.0, 3.0)); int far = int(far_counts[level] * std::clamp(double(distant_density), 0.0, 3.0)); - if (near == near_count && far == far_count) return; - near_count = near; far_count = far; + int dust = int(dust_counts[level] * std::clamp(double(dust_density), 0.0, 3.0)); + if (near == near_count && far == far_count && dust == dust_count) return; + near_count = near; far_count = far; dust_count = dust; std::vector vertices; - vertices.reserve((near + far) * 6); + vertices.reserve((near + far + dust) * 6); const float corners[6][2] = {{0,-1},{1,-1},{1,1},{0,-1},{1,1},{0,1}}; // Independent seeds retain both populations when either density changes. - for (int layer = 0; layer < 2; ++layer) - for (int i = 0; i < (layer ? near : far); ++i) + // Buffer order: distant stars, dust, passing stars. Changing any density + // retains the seeds of the other layers, including their colour balance. + const int counts[] = {far, dust, near}; + const uint32_t salts[] = {419, 19139, 7183}; + for (int layer = 0; layer < 3; ++layer) + for (int i = 0; i < counts[layer]; ++i) { - uint32_t base = i * 4 + (layer ? 7183 : 419); + uint32_t base = (i * 4 + salts[layer]) ^ (scene_seed * 0x9e3779b9u); float seed[] = {2 * star_random(base) - 1, 2 * star_random(base + 1) - 1, star_random(base + 2), star_random(base + 3)}; for (const auto& corner : corners) @@ -104,23 +137,35 @@ { const auto now = std::chrono::steady_clock::now(); const double dt = std::clamp(std::chrono::duration(now - last_frame).count(), 0.0, 0.25); + update_particles(); rendered_speed = std::clamp(double(speed), 0.0, 5.0) * (1 + boost * std::clamp(double(surge), 0.0, 2.0) * 3); if (animated()) { boost += ((transition ? 1.0 : 0.0) - boost) * (1 - std::exp(-dt / (transition ? 0.18 : 0.55))); rendered_speed = std::clamp(double(speed), 0.0, 5.0) * (1 + boost * std::clamp(double(surge), 0.0, 2.0) * 3); const double advance = dt * rendered_speed * 1.8; - travel = std::fmod(travel + advance, 12.0); + const double next_travel = travel + advance; + travel_epoch = (travel_epoch + unsigned(next_travel / 12.0)) % 4096; + travel = std::fmod(next_travel, 12.0); cloud_phase = std::fmod(cloud_phase + advance * 0.55, 64.0); // Distant stars never inherit the jump surge or foreground velocity. drift_phase = std::fmod(drift_phase + dt * double(speed) * std::clamp(double(distant_drift), 0.0, 1.0) * 0.04, 2 * M_PI); + // Event motion ignores workspace surges. Hidden/paused effects never + // consume their event clock, and speed zero freezes jets as well. + pulsar_age += dt * std::min(double(speed), 2.0); + if (pulsar_age > pulsar_duration) + { + const double remainder = pulsar_age - pulsar_duration; + prepare_pulsar(); + pulsar_age = remainder - (20 + 25 * star_random(scene_seed ^ event_number)); + } cloud_dirty = backdrop_dirty = true; } last_frame = now; - load_programs(); update_particles(); + load_programs(); const int level = std::clamp(int(quality), 0, 2); - const float scales[] = {0.25f, 1.0f / 3, 0.5f}; + const float scales[] = {0.20f, 0.25f, 1.0f / 3}; const float cloud_scale = scales[level] * target.scale; if (cloud_buffer.allocate(resolution, cloud_scale) == wf::buffer_reallocation_result_t::REALLOCATED) cloud_dirty = backdrop_dirty = true; @@ -143,6 +188,9 @@ cloud_program.uniform1f("phase", cloud_phase); cloud_program.uniform1f("cloud_density", std::clamp(double(clouds), 0.0, 2.0)); cloud_program.uniform1f("turbulence", std::clamp(double(turbulence), 0.0, 2.0)); + cloud_program.uniform1f("variation", std::clamp(double(variation), 0.0, 2.0)); + cloud_program.uniform3f("scene_offset", 64 * star_random(scene_seed), + 64 * star_random(scene_seed + 1), 64 * star_random(scene_seed + 2)); cloud_program.uniform1f("ribbons", std::clamp(double(ribbons), 0.0, 2.0)); cloud_program.uniform1f("glow", std::clamp(double(glow), 0.0, 2.0)); cloud_program.uniform1f("brightness", std::clamp(double(brightness), 0.0, 2.0)); @@ -156,38 +204,63 @@ } wf::render_target_t backdrop_target{backdrop_buffer}; backdrop_target.geometry = box; backdrop_target.scale = target.scale; - auto draw_stars = [&] (bool distant) - { - const int count = distant ? far_count : near_count; + auto draw_stars = [&] (int layer) + { + const int count = layer == 0 ? far_count : layer == 1 ? dust_count : near_count; if (!count) return; star_program.use(wf::TEXTURE_TYPE_RGBA); star_program.uniformMatrix4f("mvp", wf::gles::render_target_orthographic_projection(backdrop_target)); star_program.uniform2f("resolution", width, height); star_program.uniform1f("travel", travel); star_program.uniform1f("speed", rendered_speed); + star_program.uniform1f("epoch", travel_epoch); star_program.uniform1f("drift", drift_phase); star_program.uniform1f("brightness", std::clamp(double(brightness), 0.0, 2.0)); star_program.uniform1f("coloured_stars", std::clamp(double(coloured), 0.0, 1.0)); star_program.uniform1f("glow", std::clamp(double(glow), 0.0, 2.0)); star_program.uniform1f("backdrop_opacity", 1.0); - star_program.uniform1i("distant", distant ? 1 : 0); + star_program.uniform1i("layer", layer); GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, star_buffer)); star_program.attrib_pointer("seed",4,sizeof(hyperspace_star_t),reinterpret_cast(offsetof(hyperspace_star_t,seed))); star_program.attrib_pointer("corner",2,sizeof(hyperspace_star_t),reinterpret_cast(offsetof(hyperspace_star_t,corner))); GL_CALL(glEnable(GL_BLEND)); GL_CALL(glBlendFunc(GL_ONE,GL_ONE)); wf::gles::for_each_scissor_rect(backdrop_target,box,[&] - { GL_CALL(glDrawArrays(GL_TRIANGLES,distant ? 0 : far_count * 6,count * 6)); }); + { GL_CALL(glDrawArrays(GL_TRIANGLES,layer == 0 ? 0 : layer == 1 ? far_count * 6 : + (far_count + dust_count) * 6,count * 6)); }); GL_CALL(glDisable(GL_BLEND)); star_program.deactivate(); GL_CALL(glBindBuffer(GL_ARRAY_BUFFER,0)); }; if (backdrop_dirty) { wf::gles::bind_render_buffer(backdrop_target); wf::gles::for_each_scissor_rect(backdrop_target,box,[&] { OpenGL::clear({0,0,0,1}); }); - draw_stars(true); + draw_stars(0); + if (double(pulsars) > 0 && pulsar_age >= 0) + { + const float size = pulsar_size * height * 1.4f; + const float x = pulsar_x * width, y = pulsar_y * height; + const GLfloat vertices[] = {x-size,y-size,x+size,y-size,x+size,y+size, + x-size,y-size,x+size,y+size,x-size,y+size}; + pulsar_program.use(wf::TEXTURE_TYPE_RGBA); + pulsar_program.uniformMatrix4f("mvp", wf::gles::render_target_orthographic_projection(backdrop_target)); + pulsar_program.uniform2f("resolution", width, height); + pulsar_program.uniform2f("centre", x, y); + pulsar_program.uniform1f("size", pulsar_size); + pulsar_program.uniform1f("projection", pulsar_projection); + pulsar_program.uniform1f("angle", pulsar_angle + pulsar_age * pulsar_rotation); + // Smooth fades and a quiet breathing core, never a strobe. + const double fade = std::min({1.0, pulsar_age / 2.5, (pulsar_duration - pulsar_age) / 3.5}); + pulsar_program.uniform1f("strength", std::clamp(double(pulsars), 0.0, 2.0) * + std::clamp(double(brightness), 0.0, 2.0) * fade * fade); + pulsar_program.attrib_pointer("position", 2, 0, vertices); + GL_CALL(glEnable(GL_BLEND)); GL_CALL(glBlendFunc(GL_ONE,GL_ONE)); + wf::gles::for_each_scissor_rect(backdrop_target,box,[&] { GL_CALL(glDrawArrays(GL_TRIANGLES,0,6)); }); + GL_CALL(glDisable(GL_BLEND)); pulsar_program.deactivate(); + } + draw_stars(1); auto clouds = wf::gles_texture_t::from_aux(cloud_buffer); wf::gles::for_each_scissor_rect(backdrop_target,box,[&] { OpenGL::render_texture(clouds,backdrop_target,box); }); GL_CALL(glDisable(GL_BLEND)); - draw_stars(false); + draw_stars(2); backdrop_dirty = false; } // Composite once so 50% opacity really is half of the complete effect, --- a/plugins/cube/hyperspace-shaders.tpp +++ b/plugins/cube/hyperspace-shaders.tpp @@ -12,7 +12,8 @@ precision highp float; uniform vec2 resolution; uniform vec3 tint; -uniform float phase, cloud_density, turbulence, ribbons, glow, brightness, energy; +uniform float phase, cloud_density, turbulence, variation, ribbons, glow, brightness, energy; +uniform vec3 scene_offset; uniform int detail; varying vec2 uv; @@ -46,30 +47,47 @@ void main() { vec2 p=(uv-0.5)*vec2(resolution.x/resolution.y,1.0)*2.0; - float r=length(p), angle=atan(p.y,p.x); - float depth=2.6/max(r,0.085); - vec2 cylinder=vec2(cos(angle),sin(angle)); - vec3 shell=vec3(cylinder*2.4,depth*0.55+phase); - float bend=(noise3(vec3(shell.xy*0.5,shell.z))*2.0-1.0)*turbulence; - float mist=fbm(shell+vec3(cylinder*bend,bend)); - float inner=fbm(vec3(cylinder*3.8,depth*0.85+phase*2.0)+vec3(11,23,7)); - float centre=smoothstep(0.045,0.35,r); - float edge=1.0/(1.0+r*r*0.08); - float clouds=smoothstep(0.27,0.76,mist)*cloud_density; - float veils=smoothstep(0.34,0.86,inner)*cloud_density; - float ribbon_angle=angle*9.0+depth*0.075+bend*1.6; - float ribbon=pow(max(0.0,0.5+0.5*cos(ribbon_angle)),24.0); - ribbon*=0.15+0.85*smoothstep(0.24,0.84,inner); - vec3 ice=mix(tint,vec3(0.72,0.88,1.0),0.22); - vec3 violet=mix(tint,vec3(0.34,0.22,0.60),0.20); - vec3 color=(tint*clouds*0.16+violet*veils*0.085+ice*ribbon*ribbons*0.20)*centre*edge; - // Soft destination light and a broad halo, without a flashing jump cut. - float halo=exp(-r*r*2.8), core=exp(-r*r*140.0); - color+=(ice*halo*0.055+vec3(0.86,0.94,1.0)*core*0.40)*glow; + float r=length(p); + vec3 color=vec3(0.0); + float transmittance=1.0; + // Perspective slices through a seeded three-dimensional density field. + // Broad coverage leaves holes; warped finer noise makes torn filaments. + // Each slice projects at a different depth rather than following a ring. + for (int i=0;i<8;i++) + { + if (i < 4+detail*2) + { + float distance=2.0+float(i)*float(i)*0.42; + vec3 world=vec3(p*distance*0.46,distance+phase); + vec3 coarse=vec3(world.xy*0.19,world.z)+scene_offset; + float cover=noise3(coarse); + float bend=noise3(vec3(world.xy*0.37,world.z)+scene_offset.yzx); + vec3 fine=vec3(world.xy*0.72,world.z*2.0)+scene_offset.zxy; + fine.xy+=vec2(bend-0.5,cover-0.5)*turbulence*2.4; + float structure=fbm(fine); + float gaps=smoothstep(0.29+variation*0.10,0.77,cover); + float wisps=smoothstep(0.26,0.73,structure); + float filament=pow(max(0.0,1.0-abs(structure*2.0-1.0)),6.0); + // An uneven flight corridor opens naturally towards the horizon. + float corridor=smoothstep(0.55,1.65,length(world.xy)+bend*0.65); + float density=gaps*wisps*corridor*cloud_density; + float alpha=1.0-exp(-density*(0.18+variation*0.07)); + vec3 shade=mix(tint*0.48,vec3(0.59,0.67,0.76),cover*0.28); + vec3 lit=mix(tint,vec3(0.78,0.86,0.94),0.42); + float highlights=pow(max(0.0,structure-0.35)*1.65,3.0); + // Ribbons follow the random density field, without equally spaced + // spokes. They disappear with the cloud density control as well. + vec3 emission=shade*0.34+lit*(highlights*0.60+filament*ribbons*0.22); + color+=transmittance*emission*alpha; + transmittance*=1.0-alpha; + } + } + vec3 ice=mix(tint,vec3(0.72,0.88,1.0),0.30); + float halo=exp(-r*r*2.8),core=exp(-r*r*140.0); + color+=(ice*halo*0.035+vec3(0.86,0.94,1.0)*core*0.36)*glow; color*=brightness*energy; color=vec3(1.0)-exp(-color); - float absorption=clamp((clouds*0.36+veils*0.16)*centre*edge,0.0,0.85); - gl_FragColor=vec4(color,absorption); + gl_FragColor=vec4(color,1.0-transmittance); } )"; @@ -80,41 +98,63 @@ attribute vec2 corner; uniform mat4 mvp; uniform vec2 resolution; -uniform float travel, speed, drift, brightness, coloured_stars; -uniform int distant; +uniform float travel, speed, drift, brightness, coloured_stars, epoch; +uniform int layer; varying vec2 local; varying float trail_length, radius, luminosity; varying vec3 star_color; +float random2(vec2 p) +{ + vec3 q=fract(vec3(p.xyx)*vec3(0.1031,0.1030,0.0973)); + q+=dot(q,q.yzx+33.33); + return fract((q.x+q.y)*q.z); +} void main() { vec2 head,tail; float pixel_scale=clamp(resolution.y/1080.0,0.65,2.0); - if (distant==1) + float spectral_seed=seed.w; + if (layer==0) { vec2 offset=vec2(sin(drift),cos(drift)-1.0)*resolution.y*0.07; head=mod((seed.xy*0.5+0.5)*resolution+offset,resolution)-resolution*0.5; tail=head; - radius=(0.65+1.0*seed.w*seed.w)*pixel_scale; - luminosity=brightness*(0.18+0.72*pow(seed.w,3.0))*(0.94+0.06*sin(drift*8.0+seed.z*50.0)); + radius=(0.55+1.15*seed.w*seed.w)*pixel_scale; + luminosity=brightness*(0.14+0.76*pow(seed.w,3.0))*(0.94+0.06*sin(drift*8.0+seed.z*50.0)); } else { float z=mod(seed.z*12.0-travel,12.0)+0.45; - vec2 world=seed.xy*vec2(resolution.x/resolution.y,1.0)*6.0; + // Resample only when a particle returns beyond the horizon. The + // epoch clock wraps coherently for particles still on this pass. + float cycle=mod(epoch+step(seed.z*12.0,travel),4096.0); + vec2 key=seed.xy*37.0+vec2(cycle,cycle*0.37); + vec2 position=vec2(random2(key),random2(key+vec2(17,43)))*2.0-1.0; + float size_seed=random2(key+vec2(91,29)); + spectral_seed=random2(key+vec2(7,61)); + vec2 world=position*vec2(resolution.x/resolution.y,1.0)*(layer==1 ? 4.0 : 6.0); head=world/z*resolution.y*0.5; - tail=world/(z+speed*0.42)*resolution.y*0.5; - radius=(1.0+2.0*pow(seed.w,3.0))*pixel_scale*(0.7+1.0/(z+0.5)); - float fade=smoothstep(0.45,0.9,z)*(1.0-smoothstep(11.0,12.45,z)); - luminosity=brightness*fade*(0.35+1.25*pow(seed.w,4.0)); + float exposure=layer==1 ? 0.065 : 0.42; + tail=world/(z+speed*exposure)*resolution.y*0.5; + float fade=smoothstep(0.45,1.1,z)*(1.0-smoothstep(10.0,12.45,z)); + if (layer==1) + { + radius=(0.38+0.80*size_seed*size_seed)*pixel_scale*(0.75+0.7/(z+0.5)); + luminosity=brightness*fade*(0.04+0.32*pow(size_seed,3.0)); + } else + { + radius=(0.8+2.2*pow(size_seed,4.0))*pixel_scale*(0.7+1.0/(z+0.5)); + luminosity=brightness*fade*(0.22+1.45*pow(size_seed,4.0)); + } } - vec2 direction=normalize(head-tail+vec2(0.00001)), normal=vec2(-direction.y,direction.x); - trail_length=min(length(head-tail),resolution.y*0.35); + vec2 direction=normalize(head-tail+vec2(0.00001)),normal=vec2(-direction.y,direction.x); + trail_length=min(length(head-tail),resolution.y*(layer==1 ? 0.065 : 0.35)); tail=head-direction*trail_length; local=vec2(corner.x*(trail_length+2.0*radius)-radius,corner.y*radius); vec2 pixel=resolution*0.5+tail+direction*local.x+normal*local.y; gl_Position=mvp*vec4(pixel,0.0,1.0); - float chance=fract(seed.w*31.37), spectral=fract(seed.w*17.0); - star_color=vec3(1.0); - if (chance + + + +