Files
MapLibreParticleLab/include/mln/shaders/mtl/particle_gpu_validation.hpp

143 lines
12 KiB
C++

#pragma once
#include <mln/shaders/particle_gpu_validation_ubo.hpp>
#include <mln/shaders/shader_source.hpp>
#include <mln/shaders/mtl/shader_program.hpp>
#include <array>
namespace mln::shaders {
// This is intentionally a backend-only translation of the formal particle
// shader. ParticleGpuValidationUBO remains the single C++ ABI shared by GL and
// Metal; the MSL declaration below is kept vec4/mat4-only to preserve its
// alignas(16), 31 * 16-byte contract.
constexpr auto particleGpuValidationShaderPrelude = R"(
enum {
idParticleGpuValidationUBO = drawableReservedUBOCount,
particleGpuValidationUBOCount
};
struct alignas(16) ParticleGpuValidationUBO {
float4 pass; float4 marker; float4 mask; float4 visual;
float4 fieldBounds; float4 respawnBounds; float4 fieldScale; float4 advection;
float4 particleColor; float4 trailColor; float4 weatherStops[8];
float4 weatherColors[8]; float4 weatherRamp; float4x4 matrix;
};
static_assert(sizeof(ParticleGpuValidationUBO) == 31 * 16, "wrong size");
)";
template <>
struct ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::Metal> {
static constexpr auto name = "ParticleGpuValidationShader";
static constexpr auto vertexMainFunction = "vertexMain";
static constexpr auto fragmentMainFunction = "fragmentMain";
static const std::array<AttributeInfo, 1> attributes;
static constexpr std::array<AttributeInfo, 0> instanceAttributes{};
static const std::array<TextureInfo, 5> textures;
static constexpr auto prelude = particleGpuValidationShaderPrelude;
static constexpr auto source = R"(
struct VertexStage { short2 pos [[attribute(0)]]; };
struct FragmentStage { float4 position [[position, invariant]]; float2 uv; float2 world; float2 velocity; float valid; };
float2 fieldUv(float2 world, device const ParticleGpuValidationUBO& u) {
return float2((world.x-u.fieldBounds.x)/(u.fieldBounds.z-u.fieldBounds.x),
(world.y-u.fieldBounds.y)/(u.fieldBounds.w-u.fieldBounds.y));
}
bool validFieldUv(float2 uv) { return all(uv >= float2(0.0)) && all(uv <= float2(1.0)); }
bool finite2(float2 value) { return all(isfinite(value)) && all(abs(value) <= float2(3.4e38)); }
bool resolveAdvection(float4 a, float4 b, device const ParticleGpuValidationUBO& u, thread float2& out) {
float2 mixed=mix(((floor(a.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.x,((floor(b.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.y,u.marker.x);
if (!finite2(mixed)) return false;
if (u.advection.x > .5) { float l=length(mixed); if (!isfinite(l)||l>3.4e38||l<=u.advection.z) return false; out=mixed/l*u.advection.y; }
else { out=mixed; return length(out)>0.00001; }
return true;
}
float particleHash(float2 v) { return fract(sin(dot(v,float2(127.1,311.7)))*43758.5453123); }
float2 respawnSeed(float2 uv, float2 entropy, float attempt, device const ParticleGpuValidationUBO& u) {
float2 salt=entropy*float2(13.17,19.91)+uv*float2(97.23,53.71)+attempt;
return mix(u.respawnBounds.xy,u.respawnBounds.zw,float2(particleHash(salt),particleHash(salt.yx+41.29)));
}
float mercatorLatitudeRadians(float y) { return atan(sinh(M_PI_F*(1.0-2.0*y))); }
FragmentStage vertex vertexMain(thread const VertexStage in [[stage_in]],
device const ParticleGpuValidationUBO& u [[buffer(idParticleGpuValidationUBO)]],
texture2d<float> previous [[texture(0)]], texture2d<float> field [[texture(2)]],
texture2d<float> nextField [[texture(3)]], texture2d<float> state [[texture(4)]],
sampler previousSampler [[sampler(0)]], sampler fieldSampler [[sampler(2)]],
sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) {
// a_pos is an unnormalized short2, matching the OpenGL contract.
float2 pos=float2(in.pos);
FragmentStage out={}; out.uv=pos; out.world=pos; out.valid=1.0;
if (u.pass.x > 5.5) {
float corner=glMod(pos.x,4.0);
float2 index=float2(floor(pos.x*.25),pos.y);
float2 stateUv=(index+.5)/u.mask.xy;
float4 old=previous.sample(previousSampler,stateUv), current=state.sample(stateSampler,stateUv);
out.uv=stateUv; out.world=current.rg;
bool stateOk=current.a>.5&&distance(old.rg,current.rg)>1e-9;
float2 uv=fieldUv(current.rg,u);
float4 a=field.sample(fieldSampler,uv), b=nextField.sample(nextFieldSampler,uv);
float2 advection;
bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);
out.velocity=float2(advection.x,-advection.y);
out.valid=stateOk&&ok&&length(out.velocity)>0.00001?1.0:0.0;
float4 clip=u.matrix*float4(current.rg*8192.0,0,1);
float2 cornerOffset=corner<.5?float2(-1,-1):(corner<1.5?float2(1,-1):(corner<2.5?float2(-1,1):float2(1,1)));
float2 halfPixelNdc=float2(1.0/max(u.fieldScale.w,1.0),1.0/max(u.advection.w,1.0));
clip.xy+=cornerOffset*halfPixelNdc*clip.w;
out.position=out.valid<.5?float4(-2,-2,0,1):clip;
} else if (u.pass.x > 4.5) {
out.position=u.matrix*float4(pos*8192.0,0,1);
} else if (u.pass.x < 1.5) {
// Metal render targets are addressed from the top-left. Flip the
// fullscreen varying so the same logical State/Trail texel is sampled
// on the next ping-pong pass. Particle texel indices remain unflipped.
out.uv=float2(pos.x,1.0-pos.y);
out.position=float4(pos*2.0-1.0,0,1);
} else if (u.pass.x > 3.5) {
float endpoint=glMod(pos.x,2.0)*2.0-1.0;
float2 index=float2(floor(pos.x*.5),pos.y);
float2 stateUv=(index+.5)/u.mask.xy;
float4 old=previous.sample(previousSampler,stateUv), current=state.sample(stateSampler,stateUv);
out.uv=stateUv; out.world=current.rg;
bool stateOk=current.a>.5&&distance(old.rg,current.rg)>1e-9;
float2 uv=fieldUv(current.rg,u); float4 a=field.sample(fieldSampler,uv), b=nextField.sample(nextFieldSampler,uv); float2 advection;
bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);
out.velocity=float2(advection.x,-advection.y);
out.valid=stateOk&&ok&&length(out.velocity)>0.00001?1.0:0.0;
out.position=out.valid<.5?float4(-2,-2,0,1):u.matrix*float4((endpoint<0?old.rg:current.rg)*8192.0,0,1);
} else {
if (u.mask.z>.5) {
// Composite samples the viewport-sized Trail render target.
out.uv=float2(pos.x,1.0-pos.y);
out.position=float4(pos*2.0-1.0,0,1);
} else {
out.position=u.matrix*float4(pos*8192.0,0,1);
}
}
return out;
}
float3 speedColor(float speed) { float t=clamp(speed*.75,0.0,1.0); return mix(mix(float3(.10,.45,1),float3(.10,.95,.72),smoothstep(0.0,.55,t)),float3(1,.48,.12),smoothstep(.45,1,t)); }
float4 ramp(float value, device const ParticleGpuValidationUBO& u) { int count=int(u.weatherRamp.x); if(count<2){float t=clamp(value/30.0,0.0,1.0);return float4(mix(mix(float3(.08,.18,.65),float3(.08,.80,.78),smoothstep(0.,.55,t)),float3(.98,.22,.10),smoothstep(.45,1.,t)),1); } float4 c=u.weatherColors[0]; for(int i=1;i<8&&i<count;i++){float lo=u.weatherStops[i-1].x,hi=u.weatherStops[i].x;if(value<=hi)return mix(c,u.weatherColors[i],clamp((value-lo)/(hi-lo),0.,1.));c=u.weatherColors[i];}return c; }
float4 fragment fragmentMain(FragmentStage in [[stage_in]], device const ParticleGpuValidationUBO& u [[buffer(idParticleGpuValidationUBO)]],
texture2d<float> previous [[texture(0)]], texture2d<float> mask [[texture(1)]], texture2d<float> field [[texture(2)]], texture2d<float> nextField [[texture(3)]], texture2d<float> state [[texture(4)]],
sampler previousSampler [[sampler(0)]], sampler maskSampler [[sampler(1)]], sampler fieldSampler [[sampler(2)]], sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) {
float mode=u.pass.x;
if(mode>5.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);}
if(mode>4.5){float2 uv=fieldUv(in.world,u); if(!validFieldUv(uv)) discard_fragment(); float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(a.a<.5||b.a<.5)discard_fragment();float value=u.weatherRamp.y>.5?length(mix(((floor(a.rg*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.rg*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x)):mix(((floor(a.r*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.r*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x);if(!isfinite(value))discard_fragment();float4 c=ramp(value,u);float alpha=c.a*u.visual.x;return float4(c.rgb*alpha,alpha);}
float4 old=previous.sample(previousSampler,in.uv);
if(mode<.5){
float2 position=u.mask.w>.5?mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv):old.rg; float age=old.b; bool initial=u.mask.w>.5; bool respawn=initial; float2 advection; float maxAge=u.marker.z*mix(.75,1.25,particleHash(in.uv+float2(17.31,83.17)));
if(u.mask.z>.5){age=initial?particleHash(in.uv+float2(71.19,29.47))*maxAge:old.b+u.pass.w;float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);respawn=initial||!ok||age>=maxAge||(u.fieldScale.z>.5&&!initial&&(position.x<u.respawnBounds.x||position.x>u.respawnBounds.z||position.y<u.respawnBounds.y||position.y>u.respawnBounds.w));if(!respawn){float lat=mercatorLatitudeRadians(position.y);float2 n=position+float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));float2 nuv=fieldUv(n,u);float4 na=field.sample(fieldSampler,nuv),nb=nextField.sample(nextFieldSampler,nuv);if(validFieldUv(nuv)&&na.a>=.5&&nb.a>=.5&&resolveAdvection(na,nb,u,advection))position=n;else respawn=true;}if(respawn){bool found=false;for(int i=0;i<3;i++){float2 c=respawnSeed(in.uv,old.rg,float(i),u);float2 cuv=fieldUv(c,u);float4 ca=field.sample(fieldSampler,cuv),cb=nextField.sample(nextFieldSampler,cuv);if(validFieldUv(cuv)&&ca.a>=.5&&cb.a>=.5&&resolveAdvection(ca,cb,u,advection)){position=c;found=true;break;}}if(!found)position=respawnSeed(in.uv,old.rg,3.,u);if(!initial)age=0.;}return float4(position.x,position.y,age,respawn?0.:1.);}
float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(!validFieldUv(uv)||a.a<.5||b.a<.5||!resolveAdvection(a,b,u,advection)){position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);advection=float2(0);}float lat=mercatorLatitudeRadians(position.y);position+=float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));if(!validFieldUv(fieldUv(position,u))||!finite2(advection))position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);return float4(position.x,position.y,fract(old.b+.03125*u.pass.y),1.);
}
if(mode<1.5){if(u.mask.z>.5){float f=pow(clamp(u.visual.w,.0001,1.),u.pass.y*u.marker.w);return float4(old.r*f,0,0,1);}float marker=0.;float2 samples[4]={float2(.125),float2(.375),float2(.625),float2(.875)};for(int i=0;i<4;i++)marker=max(marker,1.-smoothstep(0.,u.marker.z,distance(in.uv,state.sample(stateSampler,samples[i]).rg)));return float4(min(old.rgb*u.marker.w+marker*float3(.08,.55,1),float3(1)),1);}
if(mode>3.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);}
float ocean=mask.sample(maskSampler,in.world).a;if(u.pass.z>.5&&ocean<.5)discard_fragment();if(u.mask.z>.5){bool current=old.g>.5;float4 base=current?u.particleColor:u.trailColor;if(current&&u.marker.y>.5)base.rgb=speedColor(old.b*30.);float alpha=clamp(old.r*base.a*(current?u.visual.x:u.visual.z),0.,1.);return float4(base.rgb*alpha,alpha);}float intensity=max(old.r,max(old.g,old.b));float stripe=.55+.45*sin((in.world.x+in.world.y)*100.+u.pass.y*.07);float anchors=0.;float2 a[4]={float2(.5,.5),float2(.25,.5),float2(.02,.5),float2(.98,.5)};for(int i=0;i<4;i++)anchors=max(anchors,1.-smoothstep(0.,.008,distance(in.world,a[i])));float3 color=u.pass.w<1.5?float3(.10,.70,1.):float3(.35,1.,.45);return float4(mix(color*stripe,float3(1.,.82,.15),anchors),.28+min(intensity,.35)*.35+anchors*.35);
}
)";
};
} // namespace mln::shaders