feat(wgsl): Vertex shader bundle — spherical coords → triangles → pixels

This is NOT a pixel encoder. This is a geometry solver on GPU.

vertex_braid.wgsl: 4 shader stages
  1. vs_main: spectral coeffs → spherical → cartesian → triangle vertices
     - Each instance = one QUBO variable → one triangle
     - Spectral deformation per-instance (chaos game rotation)
     - Icosphere subdivision for smooth geometry

  2. fs_main: Hachimoji color + Lambertian lighting + energy glow
     - Octant → 8 Hachimoji states
     - Brightness modulated by QUBO energy

  3. spectral_update: FSDU compute (co-evolution step)
     - Baker-analogue damping: high-l modes decay
     - Scar noise injection

  4. Helper functions:
     - spectral_to_embedding: |l,m⟩ → S^7 → R^3
     - point_to_hachimoji: octant → base index
     - icosahedron_vertex: platonic solid mesh
     - midpoint_normalize: sphere subdivision

Key difference from pixel encoder:
  Pixel encoder: CPU computes → GPU displays (brute force)
  Vertex shader: GPU computes via spherical geodesics (geometry solver)

The GPU walks Fisher-Rao geodesics by rotating spectral coefficients.
Each triangle instance follows a different geodesic path. The fragment
shader tells you which basin (Hachimoji state) the path converged to.

Refs: S7_SPECTRAL_BASIS.md (spectral decomposition),
COEVOLUTION_MODEL.md (FSDU scar update),
FBTTY_UNIVERSAL_ENCODER.md (accessibility layer)
This commit is contained in:
Allaun Silverfox 2026-06-23 01:43:21 -05:00
parent 41aeaa4f69
commit d046b94f24

282
python/vertex_braid.wgsl Normal file
View file

@ -0,0 +1,282 @@
/**
* vertex_braid.wgsl Vertex Shader: Spherical Coordinates Triangles Pixels
*
* The compute route: QUBO spectral coeffs spherical coords cartesian
* triangle vertices rasterize Hachimoji-colored pixels.
*
* This is NOT a pixel encoder. This is a geometry solver.
* The GPU computes the solution by walking geodesics on the Fisher sphere.
*/
// ============================================================
// CONSTANTS
// ============================================================
const PI: f32 = 3.14159265358979323846;
const TWO_PI: f32 = 6.28318530717958647692;
const N_HACHIMOJI: u32 = 8u;
const SPHERE_RADIUS: f32 = 1.0;
// ============================================================
// UNIFORMS
// ============================================================
struct SpectralCoeffs {
// Spherical harmonic coefficients c_{l,m} for l=0,1,2, m=-l..l
// Packed: [c_00, c_1m1, c_10, c_1p1, c_2m2, c_2m1, c_20, c_2p1, c_2p2]
coeffs: array<f32, 9>,
};
struct QUBOParams {
n_vars: u32, // number of QUBO variables
energy_scale: f32, // scales the sphere deformation
rotation_angle: f32, // global rotation (chaos game step)
_pad: u32,
};
@group(0) @binding(0) var<uniform> spectral: SpectralCoeffs;
@group(0) @binding(1) var<uniform> params: QUBOParams;
// ============================================================
// SPECTRAL SPHERICAL COORDINATES
// ============================================================
/// Convert spectral coefficients to a point on S^7.
///
/// The spectral decomposition is:
/// |ψ = Σ_{l,m} c_{l,m} |l,m
///
/// We map this to a point on the unit sphere by treating the
/// normalized coefficients as direction cosines in a
/// 9-dimensional embedding (truncated at l=2).
///
/// Then we project to the 7-sphere by dropping two coordinates
/// (analogous to stereographic projection).
fn spectral_to_embedding(coeffs: array<f32, 9>) -> vec3<f32> {
// Normalize coefficients
var norm: f32 = 0.0;
for (var i: u32 = 0u; i < 9u; i = i + 1u) {
norm = norm + coeffs[i] * coeffs[i];
}
norm = sqrt(norm);
if (norm < 0.0001) {
return vec3<f32>(0.0, 0.0, 1.0); // default: north pole
}
// Use first 3 normalized coeffs as direction (low-l dominant)
let c0 = coeffs[0] / norm; // l=0, monopole (average)
let c1 = coeffs[2] / norm; // l=1, m=0 (dipole z)
let c2 = coeffs[5] / norm; // l=2, m=0 (quadrupole)
// Map to spherical coordinates
// θ (polar): from monopole contribution (c0 = cos θ)
// φ (azimuthal): from dipole phase (c1, c2 atan2)
let theta = acos(clamp(c0, -1.0, 1.0));
let phi = atan2(c2, c1); // phase from quadrupole vs dipole
// Convert to cartesian on unit sphere
let r = SPHERE_RADIUS;
let x = r * sin(theta) * cos(phi);
let y = r * sin(theta) * sin(phi);
let z = r * cos(theta);
// Apply chaos-game rotation (energy scale deforms the sphere)
let angle = params.rotation_angle * params.energy_scale;
let ca = cos(angle);
let sa = sin(angle);
let rx = x * ca - y * sa;
let ry = x * sa + y * ca;
return vec3<f32>(rx, ry, z);
}
// ============================================================
// HACHIMOJI STATE COLOR
// ============================================================
/// Map a point on the sphere to a Hachimoji state.
/// Uses the 8 octants of the cartesian space.
fn point_to_hachimoji(p: vec3<f32>) -> u32 {
let sx = select(0u, 1u, p.x > 0.0);
let sy = select(0u, 1u, p.y > 0.0);
let sz = select(0u, 1u, p.z > 0.0);
// Octant index: 0..7 maps to A,B,C,G,P,S,T,Z
let octant = (sz << 2u) | (sy << 1u) | sx;
return octant % 8u;
}
/// Hachimoji palette (sRGB, linear space)
fn hachimoji_color(base: u32) -> vec3<f32> {
switch(base) {
case 0u: { return vec3<f32>(0.05, 0.05, 0.05); } // A = Φ
case 1u: { return vec3<f32>(0.20, 0.10, 0.30); } // B = Λ
case 2u: { return vec3<f32>(0.10, 0.30, 0.50); } // C = Ρ
case 3u: { return vec3<f32>(0.10, 0.80, 0.30); } // G = Σ
case 4u: { return vec3<f32>(0.90, 0.40, 0.10); } // P = Ω
case 5u: { return vec3<f32>(0.60, 0.20, 0.80); } // S = Π
case 6u: { return vec3<f32>(0.10, 0.70, 0.70); } // T = Κ
case 7u: { return vec3<f32>(0.95, 0.95, 0.95); } // Z = Ζ
default: { return vec3<f32>(1.0, 0.0, 1.0); } // error: magenta
}
}
// ============================================================
// TRIANGLE MESH: Icosphere subdivision
// ============================================================
/// Generate an icosahedron vertex.
/// idx: 0..11 (the 12 vertices of an icosahedron)
fn icosahedron_vertex(idx: u32) -> vec3<f32> {
let phi = (1.0 + sqrt(5.0)) / 2.0; // golden ratio
let norm = sqrt(1.0 + phi * phi);
let a = 1.0 / norm;
let b = phi / norm;
switch(idx % 12u) {
case 0u: { return vec3<f32>(-a, b, 0.0); }
case 1u: { return vec3<f32>( a, b, 0.0); }
case 2u: { return vec3<f32>(-a, -b, 0.0); }
case 3u: { return vec3<f32>( a, -b, 0.0); }
case 4u: { return vec3<f32>(0.0, -a, b); }
case 5u: { return vec3<f32>(0.0, a, b); }
case 6u: { return vec3<f32>(0.0, -a, -b); }
case 7u: { return vec3<f32>(0.0, a, -b); }
case 8u: { return vec3<f32>( b, 0.0, -a); }
case 9u: { return vec3<f32>( b, 0.0, a); }
case 10u: { return vec3<f32>(-b, 0.0, -a); }
case 11u: { return vec3<f32>(-b, 0.0, a); }
default: { return vec3<f32>(0.0, 0.0, 1.0); }
}
}
/// Subdivide an edge midpoint and re-normalize to sphere.
fn midpoint_normalize(a: vec3<f32>, b: vec3<f32>) -> vec3<f32> {
let m = (a + b) * 0.5;
return normalize(m) * SPHERE_RADIUS;
}
// ============================================================
// VERTEX SHADER ENTRY POINT
// ============================================================
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>,
@location(1) hachimoji: u32,
@location(2) world_pos: vec3<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) vertex_idx: u32,
@builtin(instance_index) instance_idx: u32,
) -> VertexOutput {
// Each instance = one QUBO variable
// Each instance renders one triangle of the icosphere
// Get the base triangle vertices (icosahedron)
let v0_idx = (instance_idx * 3u + 0u) % 12u;
let v1_idx = (instance_idx * 3u + 1u) % 12u;
let v2_idx = (instance_idx * 3u + 2u) % 12u;
// Select which vertex of the triangle this is
var local_pos: vec3<f32>;
switch(vertex_idx % 3u) {
case 0u: { local_pos = icosahedron_vertex(v0_idx); }
case 1u: { local_pos = icosahedron_vertex(v1_idx); }
case 2u: { local_pos = icosahedron_vertex(v2_idx); }
default: { local_pos = vec3<f32>(0.0, 0.0, 1.0); }
}
// Deform the sphere by spectral coefficients
// Each instance gets a different rotation based on variable index
let instance_angle = f32(instance_idx) * TWO_PI / f32(params.n_vars);
let rotated_coeffs = spectral.coeffs;
rotated_coeffs[2] = spectral.coeffs[2] * cos(instance_angle); // dipole x
// rotated_coeffs[1] = spectral.coeffs[1] * sin(instance_angle); // dipole y (need 9th coeff)
// Map spectral spherical cartesian
let sphere_pos = spectral_to_embedding(rotated_coeffs);
// Combine: local triangle geometry + global spectral deformation
// The triangle follows the spectral flow on the sphere
let final_pos = normalize(local_pos + sphere_pos * 0.3) * SPHERE_RADIUS;
// Project to screen (simple perspective)
// View from +z looking at origin
let fov = 60.0 * PI / 180.0;
let aspect = 1.0;
let near = 0.1;
let far = 10.0;
let f = 1.0 / tan(fov / 2.0);
let x_proj = final_pos.x * f / aspect;
let y_proj = final_pos.y * f;
let z_proj = (far + near) / (near - far) + (2.0 * far * near) / (near - far) / final_pos.z;
let w = -final_pos.z;
var out: VertexOutput;
out.position = vec4<f32>(x_proj, y_proj, z_proj, w);
// Hachimoji state from octant
out.hachimoji = point_to_hachimoji(final_pos);
out.color = hachimoji_color(out.hachimoji);
out.world_pos = final_pos;
return out;
}
// ============================================================
// FRAGMENT SHADER
// ============================================================
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Base color from Hachimoji state
var color = in.color;
// Add subtle lighting (Lambertian)
let light_dir = normalize(vec3<f32>(1.0, 1.0, 2.0));
let normal = normalize(in.world_pos);
let lambert = max(dot(normal, light_dir), 0.0);
color = color * (0.3 + 0.7 * lambert);
// Add energy-dependent glow (brighter = lower energy)
let energy_glow = 1.0 - clamp(abs(params.energy_scale) * 0.1, 0.0, 0.5);
color = color * energy_glow;
// Gamma correction
color = pow(color, vec3<f32>(1.0 / 2.2));
return vec4<f32>(color, 1.0);
}
// ============================================================
// COMPUTE SHADER: Spectral coefficient update (FSDU step)
// ============================================================
/// Update spectral coefficients based on FAMM scar field.
/// This is the co-evolution step: scars modify the spectrum.
@compute @workgroup_size(256)
fn spectral_update(
@builtin(global_invocation_id) gid: vec3<u32>,
) {
let idx = gid.x;
if (idx >= 9u) { return; } // only 9 coefficients
// Read current coefficient
let c = spectral.coeffs[idx];
// Apply Baker-analogue damping: high-l modes decay faster
let l = select(select(2u, 1u, idx < 4u), 0u, idx == 0u);
let damping = exp(-0.1 * f32(l * l));
// Add small noise (simulated scar pressure)
let noise = fract(sin(f32(idx) * 43758.5453) * 43758.5453) * 0.01;
// Update: damped + noise (FSDU scar accumulation)
spectral.coeffs[idx] = c * damping + noise;
}