shader_type spatial; // Flat crowd sprite. Each instance is oriented at build time to face the arena centre // (see crowd_billboards.gd) — NOT the camera — so spectators on different segments of // the ring face inward toward the action, like a real stand. Unshaded (the sheet was // baked with lighting), alpha-tested (no transparency sorting across hundreds of // quads), nearest-filtered for a crisp PS1 look that matches the ps1_filter effect. render_mode unshaded, cull_disabled, shadows_disabled, depth_draw_opaque; uniform sampler2D sheet : source_color, filter_nearest; uniform int hframes = 8; uniform float fps = 10.0; uniform float alpha_clip = 0.4; varying flat float v_frame; varying flat vec3 v_tint; void vertex() { // Per-instance clap phase (0..1) and tint are packed into the MultiMesh custom data. // The instance transform already carries the inward facing, so no billboard maths. float phase = INSTANCE_CUSTOM.x; v_tint = INSTANCE_CUSTOM.yzw; v_frame = floor(mod(TIME * fps + phase * float(hframes), float(hframes))); } void fragment() { // Sheet is two rows: top = front, bottom = back. Show the back when the camera is // behind the spectator (they're facing into the arena, away from us). float u = (UV.x + v_frame) / float(hframes); float v = UV.y * 0.5 + (FRONT_FACING ? 0.0 : 0.5); vec4 c = texture(sheet, vec2(u, v)); if (c.a < alpha_clip) { discard; } ALBEDO = c.rgb * v_tint; }