diff --git a/python/vertex_braid.wgsl b/python/vertex_braid.wgsl new file mode 100644 index 00000000..99fce47e --- /dev/null +++ b/python/vertex_braid.wgsl @@ -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, +}; + +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 spectral: SpectralCoeffs; +@group(0) @binding(1) var 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) -> vec3 { + // 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(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(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) -> 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 { + switch(base) { + case 0u: { return vec3(0.05, 0.05, 0.05); } // A = Φ + case 1u: { return vec3(0.20, 0.10, 0.30); } // B = Λ + case 2u: { return vec3(0.10, 0.30, 0.50); } // C = Ρ + case 3u: { return vec3(0.10, 0.80, 0.30); } // G = Σ + case 4u: { return vec3(0.90, 0.40, 0.10); } // P = Ω + case 5u: { return vec3(0.60, 0.20, 0.80); } // S = Π + case 6u: { return vec3(0.10, 0.70, 0.70); } // T = Κ + case 7u: { return vec3(0.95, 0.95, 0.95); } // Z = Ζ + default: { return vec3(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 { + 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(-a, b, 0.0); } + case 1u: { return vec3( a, b, 0.0); } + case 2u: { return vec3(-a, -b, 0.0); } + case 3u: { return vec3( a, -b, 0.0); } + case 4u: { return vec3(0.0, -a, b); } + case 5u: { return vec3(0.0, a, b); } + case 6u: { return vec3(0.0, -a, -b); } + case 7u: { return vec3(0.0, a, -b); } + case 8u: { return vec3( b, 0.0, -a); } + case 9u: { return vec3( b, 0.0, a); } + case 10u: { return vec3(-b, 0.0, -a); } + case 11u: { return vec3(-b, 0.0, a); } + default: { return vec3(0.0, 0.0, 1.0); } + } +} + +/// Subdivide an edge midpoint and re-normalize to sphere. +fn midpoint_normalize(a: vec3, b: vec3) -> vec3 { + let m = (a + b) * 0.5; + return normalize(m) * SPHERE_RADIUS; +} + +// ============================================================ +// VERTEX SHADER ENTRY POINT +// ============================================================ + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec3, + @location(1) hachimoji: u32, + @location(2) world_pos: vec3, +}; + +@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; + 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(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(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 { + // Base color from Hachimoji state + var color = in.color; + + // Add subtle lighting (Lambertian) + let light_dir = normalize(vec3(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(1.0 / 2.2)); + + return vec4(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, +) { + 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; +}