struct Params { frame:vec4f, pointer:vec4f, view:vec4f, options:vec4f } struct Particle { position:vec4f, velocity:vec4f } @group(0) @binding(0) var p:Params; @group(0) @binding(1) var particles:array; @group(0) @binding(2) var render_particles:array; const TAU:f32=6.283185307; fn hash(n:f32)->f32 {return fract(sin(n*127.1+311.7)*43758.5453);} // A shared two-dimensional parameterization gives every sample a stable identity. fn surface(uv:vec2f, layer:f32, shape:f32)->vec3f { let u=uv.x; let v=uv.y; let a=u*TAU; if shape<0.5 { let r=0.85+0.36*cos(a*3.0+v*3.2)+0.16*sin(v*TAU*1.5); let ribbon=(v-0.5)*1.3; return vec3f(cos(a)*(r+ribbon*0.28),sin(a)*(r+ribbon*0.28)*0.89,sin(a*2.0+v*2.0)*0.30+ribbon*0.70+layer*0.015); } if shape<1.5 { if v<0.075 && layer<3.0 { let t=v/0.075; let x=(floor(u*5.0)/4.0-0.5)*2.55+sin(t*3.14159)*0.14; let z=-0.68+sin(t*3.14159)*0.40; let y=(layer-1.5+t)*0.49+0.17*sin(x*2.0+z*1.8)+0.19*x; return vec3f(x,y,z); } let x=(u-0.5)*2.55; let z=(v-0.5)*1.65; let y=(layer-1.5)*0.49+0.17*sin(x*2.0+z*1.8)+0.19*x; return vec3f(x,y,z); } if shape<2.5 { let angle=(u*1.68-0.84)*3.14159; let r=0.72+0.13*cos(v*TAU*1.5)+0.02*layer; return vec3f(sin(angle)*r,(v-0.5)*2.5,cos(angle)*r+0.12*sin(v*7.0)); } let cell=vec3f(floor(u*5.0)-2.0,floor(v*5.0)-2.0,layer-1.5); let q=vec2f(fract(u*5.0),fract(v*5.0)); return cell*0.45+vec3f(cos(q.x*TAU)*sin(q.y*3.14159),sin(q.x*TAU)*sin(q.y*3.14159),cos(q.y*3.14159))*0.125; } fn sample_hash(n:u32)->f32 { var x=n*747796405u+2891336453u; x=((x>>((x>>28u)+4u))^x)*277803737u; x=(x>>22u)^x; return f32(x>>8u)/16777216.0; } fn identity(id:u32)->vec3f { return vec3f(sample_hash(id*2u+11u),sample_hash(id*2u+79u),f32(id%4u)); } fn rotate(q:vec3f)->vec3f {let a=-0.38;let b=0.16; let r=vec3f(q.x*cos(a)+q.z*sin(a),q.y,-q.x*sin(a)+q.z*cos(a)); return vec3f(r.x,r.y*cos(b)-r.z*sin(b),r.y*sin(b)+r.z*cos(b));} @compute @workgroup_size(128) fn simulate(@builtin(global_invocation_id) invocation:vec3u) { let id=invocation.x;if id>=u32(p.frame.w) {return;} let key=identity(id);let target_position=surface(key.xy,key.z,p.frame.z); let du=surface(key.xy+vec2f(0.001,0),key.z,p.frame.z)-target_position; let dv=surface(key.xy+vec2f(0,0.001),key.z,p.frame.z)-target_position; let tangent=normalize(du+vec3f(0.000001)); let normal=normalize(cross(du,dv)+vec3f(0.000001)); let breath=sin(p.frame.x*0.23+key.x*13.0+key.y*8.0)*0.009*p.view.w; let goal=target_position+normal*breath; if p.pointer.w>0.5 {particles[id].position=vec4f(goal,1);particles[id].velocity=vec4f(0);return;} var pos=particles[id].position.xyz;var vel=particles[id].velocity.xyz; let diff=goal-pos; let aspect=p.view.x/p.view.y; let center=select(0.30,0.08,aspect<0.8); let scale=select(0.72,0.68,aspect<0.8); let screen=rotate(pos).xy*vec2f(scale/aspect,scale)+vec2f(center,0.02); let delta=screen-p.pointer.xy; let force=exp(-dot(delta,delta)*65.0)*p.pointer.z; let input_force=tangent*force*0.32; let normal_error=normal*dot(diff,normal); let accel=diff*78.0+normal_error*10.0+input_force-vel*16.0; vel+=accel*p.frame.y;pos+=vel*p.frame.y; particles[id].position=vec4f(pos,1);particles[id].velocity=vec4f(vel,0); } struct Varying { @builtin(position) position:vec4f, @location(0) uv:vec2f, @location(1) alpha:f32, @location(2) color:vec3f, @location(3) softness:f32 } @vertex fn vertex(@builtin(vertex_index) vertex_id:u32,@builtin(instance_index) id:u32)->Varying { let corners=array(vec2f(-1,-1),vec2f(1,-1),vec2f(-1,1),vec2f(-1,1),vec2f(1,-1),vec2f(1,1)); let key=identity(id);var pos=render_particles[id].position.xyz; if p.view.z>0.5 && p.view.z<1.5 {pos=surface(key.xy,key.z,p.frame.z);} let q=rotate(pos);let aspect=p.view.x/p.view.y; let center=select(0.30,0.08,aspect<0.8);let scale=select(0.72,0.68,aspect<0.8); let perspective=3.6/(3.6-q.z*0.42); let projected=q.xy*vec2f(scale/aspect,scale)*perspective+vec2f(center,0.02); let defocus=smoothstep(0.20,1.25,abs(q.z-0.12)); let rand=hash(f32(id)); let radius=(0.74+rand*0.70+defocus*4.4)*min(p.view.y/850.0,1.7); let bright=0.28+0.55*hash(f32(id)*1.73); var out:Varying; out.position=vec4f(projected+corners[vertex_id]*radius*2.0/p.view.xy,0.5,1); out.uv=corners[vertex_id];out.softness=defocus; out.alpha=bright*(1.0-defocus*0.82)*smoothstep(-0.95,-0.28,projected.x); out.color=vec3f(0.89,0.91,0.95); if p.view.z>1.5 {let speed=length(render_particles[id].velocity.xyz);out.color=mix(vec3f(0.38,0.48,0.62),vec3f(1.0,0.84,0.64),clamp(speed*0.9,0,1));out.alpha=max(out.alpha,0.16);} return out; } @fragment fn fragment(v:Varying)->@location(0) vec4f { let r=dot(v.uv,v.uv);if r>1.0 {discard;} let core=exp(-r*mix(3.6,2.1,v.softness)); return vec4f(v.color,core*v.alpha); }