struct Uniforms { view_proj: mat4x4, model: mat4x4, camera_pos: vec3, time: f32, is_light: vec4, }; @group(0) @binding(0) var uniforms: Uniforms; struct VSOut { @builtin(position) position: vec4, @location(0) normal: vec3, @location(1) world_pos: vec3, @location(2) color: vec3, }; // Vertex inputs @vertex fn vs_main( @location(0) in_pos: vec3, @location(1) in_normal: vec3, @location(2) base_pos: vec3, @location(3) scale: f32, @location(4) seed: f32, @location(5) color: vec3, ) -> VSOut { var out: VSOut; let t = uniforms.time; // --- Coherent spherical layout --- let dir = normalize(base_pos + vec3(1e-6, 0.0, 0.0)); // avoid NaN if zero let radius = 0.4; // Gentle, coherent drift so it breathes let drift = vec3( 0.06 * sin(0.9 * t + seed * 1.3), 0.05 * sin(1.1 * t + seed * 2.1), 0.06 * cos(0.7 * t + seed * 0.7) ); // Final instance position on/near the sphere //let loose = 0.2 * base_pos + drift; let tight = dir * radius + drift; //let tight = dir * radius; //let coherence = 0.8; // [0..1], or pass as a uniform //let pos_ws = mix(loose, tight, coherence); let pos_ws = tight; // --- Orient cube so its local +Z points outward (along dir) --- // Build a stable tangent basis var up = vec3(0.0, 1.0, 0.0); if (abs(dot(dir, up)) > 0.92) { up = vec3(1.0, 0.0, 0.0); } let tangent = normalize(cross(up, dir)); let bitangent = cross(dir, tangent); // Optional tiny spin around outward axis for sparkle let spin = 0.9 * t + seed * 0.9; let cs = cos(spin); let sn = sin(spin); let rot_tangent = cs * tangent + sn * bitangent; let rot_bitangent = -sn * tangent + cs * bitangent; // Rotation matrix whose columns are the local basis let R = mat3x3(rot_tangent, rot_bitangent, dir); // Scale + orient local vertex + place at spherical position let local = R * (in_pos * scale); let world_vec4 = uniforms.model * vec4(local, 1.0); let world = world_vec4 + vec4(pos_ws, 0.0); out.position = uniforms.view_proj * world; // Normal from model rotation only (ignoring per-instance rotation for now) let nmat = mat3x3( uniforms.model[0].xyz, uniforms.model[1].xyz, uniforms.model[2].xyz ); out.normal = normalize(nmat * in_normal); out.world_pos = world.xyz; out.color = color; return out; } @fragment fn fs_main(in: VSOut) -> @location(0) vec4 { // Same lighting as you had, but tint by per-instance color let material_color = in.color; let ambient_strength = 0.2; let diffuse_strength = 0.7; let specular_strength = 0.2; let shininess = 20.0; let light_pos = vec3(2.0, 2.0, 2.0); let light_color = vec3(1.0, 1.0, 1.0); let view_pos = uniforms.camera_pos; let n = normalize(in.normal); let l = normalize(light_pos - in.world_pos); let v = normalize(view_pos - in.world_pos); let r = reflect(-l, n); let ambient = ambient_strength * light_color; let diffuse = diffuse_strength * max(dot(n, l), 0.0) * light_color; let specular = specular_strength * pow(max(dot(v, r), 0.0), shininess) * light_color; let exposure = exp2(1.5); var color = (ambient + diffuse + specular) * material_color; // --- Distance-based factor (camera-space distance) --- let dist = length(view_pos - in.world_pos); let FADE_NEAR = 1.0; // start ramping here let FADE_FAR = 2.2; // fully applied by here let fade = smoothstep(FADE_NEAR, FADE_FAR, dist); // 0..1 // --- Exposure drift with distance (sign flips by mode) --- // Dark mode target exposure at far: lower; Light mode target at far: higher. let min_exp = 1.80; // far-end exposure multiplier in dark mode let max_exp = 1.35; // far-end exposure multiplier in light mode let darker = mix(1.0, min_exp, fade); // darkens with distance let brighter = mix(1.0, max_exp, fade); // brightens with distance let exp_factor = select(darker, brighter, uniforms.is_light.x > 0.0); // Apply exposure + tonemap let base_exposure = exp2(1.5); color = aces_fitted(color * base_exposure * exp_factor); // --- Optional: fade to background so distant points dissolve away --- // Background: black in dark mode, white in light mode. let bg = select(vec3(0.0), vec3(1.0), uniforms.is_light.x > 0.0); // If you want white for BOTH modes instead, use: // let bg = vec3(1.0); color = mix(color, bg, fade); return vec4(color, 1.0); } // ACES-fit tonemap (keeps highlights nicer than Reinhard) fn aces_fitted(x: vec3) -> vec3 { let a = 2.51; let b = 0.03; let c = 2.43; let d = 0.59; let e = 0.14; return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3(0.0), vec3(1.0)); }