From 603de6bbab3c07e9c3091da97d7c389929d96db5 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sun, 3 Aug 2025 22:09:17 -0700 Subject: [PATCH] evolve dave into a swarm Signed-off-by: William Casarin --- crates/notedeck_dave/src/avatar.rs | 91 ++++++++++++++++++-- crates/notedeck_dave/src/dave.wgsl | 133 +++++++++++++++++++++++------ crates/notedeck_dave/src/lib.rs | 11 ++- crates/notedeck_dave/src/mesh.rs | 23 +++++ 4 files changed, 226 insertions(+), 32 deletions(-) diff --git a/crates/notedeck_dave/src/avatar.rs b/crates/notedeck_dave/src/avatar.rs index e01ca79..1edcd9a 100644 --- a/crates/notedeck_dave/src/avatar.rs +++ b/crates/notedeck_dave/src/avatar.rs @@ -58,11 +58,65 @@ fn matrix_multiply(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] { result } +fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] { + [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t, + a[2] + (b[2] - a[2]) * t, + ] +} + +fn generate_dave_instances(instance_count: u32) -> Vec { + let mut rng = rand::rng(); + let mut instances = Vec::with_capacity(instance_count as usize); + + // Logo gradient endpoints (0–1 range) + const C0: [f32; 3] = [53.0 / 255.0, 77.0 / 255.0, 235.0 / 255.0]; // rgb(53, 77, 235) + const C1: [f32; 3] = [229.0 / 255.0, 20.0 / 255.0, 205.0 / 255.0]; // rgb(229, 20, 205) + let golden_angle = std::f32::consts::PI * (3.0 - 5.0_f32.sqrt()); + + for i in 0..instance_count { + let i_f = (i as f32) + 0.5; + let n = instance_count as f32; + + // Fibonacci sphere (unit directions) + let z = 1.0 - (2.0 * i_f) / n; + let r = (1.0 - z * z).sqrt(); + let theta = golden_angle * i_f; + + // Use base_pos as *direction*; shader will normalize/scale anyway + let base_pos = [r * theta.cos(), z, r * theta.sin()]; + + let scale = 0.03; + + //let scale = scale + scale_var + rng.random::() * scale; // slightly smaller cubes + let seed = rng.random::() * 1000.0; + + // damus logo gradient + let t_base = (z + 1.0) * 0.5; // 0..1 + let t_jitter = (rng.random::() - 0.5) * 0.06; // ±0.03 + let t = (t_base + t_jitter).clamp(0.0, 1.0); + let color = lerp3(C0, C1, t); + + instances.push(mesh::Instance { + base_pos, + scale, + seed, + color, + }); + } + + instances +} + impl DaveAvatar { pub fn new(wgpu_render_state: &egui_wgpu::RenderState) -> Self { - let device = &wgpu_render_state.device; const BINDING_SIZE: u64 = 256; + let device = &wgpu_render_state.device; + let instance_count: u32 = 256; + let instances = generate_dave_instances(instance_count); + // Create shader module with improved shader code let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("cube_shader"), @@ -115,6 +169,12 @@ impl DaveAvatar { usage: wgpu::BufferUsages::VERTEX, }); + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("cube_instances"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("cube_indices"), contents: bytemuck::cast_slice(&mesh::CUBE_INDICES), @@ -128,7 +188,7 @@ impl DaveAvatar { vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), - buffers: &[mesh::Vertex::LAYOUT], + buffers: &[mesh::Vertex::LAYOUT, mesh::Instance::LAYOUT], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { @@ -171,8 +231,10 @@ impl DaveAvatar { pipeline, bind_group, uniform_buffer, + instance_buffer, vertex_buffer, index_buffer, + instance_count, }); let initial_rot = { @@ -182,6 +244,7 @@ impl DaveAvatar { // Apply rotations (order matters) y_rotation.multiply(&x_rotation) }; + Self { rotation: initial_rot, rot_dir: Vec3::new(0.0, 0.0, 0.0), @@ -271,10 +334,10 @@ impl DaveAvatar { let projection = perspective_matrix(std::f32::consts::PI / 4.0, aspect, 0.1, 100.0); // Create view matrix (move camera back a bit) - let camera_pos = [0.0, 0.0, 3.0, 0.0]; + let camera_pos = [0.0, 0.0, 1.5]; // Right-handed look-at at origin; view is a translate by -camera_pos - let [cx, cy, cz, _] = camera_pos; + let [cx, cy, cz] = camera_pos; #[rustfmt::skip] let view = [ @@ -285,6 +348,11 @@ impl DaveAvatar { ]; let view_proj = matrix_multiply(&projection, &view); + let is_light = if ui.ctx().theme() == egui::Theme::Light { + 1.0 + } else { + -1.0 + }; // Add paint callback ui.painter().add(egui_wgpu::Callback::new_paint_callback( @@ -293,6 +361,8 @@ impl DaveAvatar { view_proj, model, camera_pos, + time: ui.ctx().input(|i| i.time as f32), + is_light: [is_light, 0.0, 0.0, 0.0], }, )); @@ -306,7 +376,9 @@ impl DaveAvatar { struct GpuData { view_proj: [f32; 16], // Model-View-Projection matrix model: [f32; 16], // Model matrix for lighting calculations - camera_pos: [f32; 4], // xyz + pad + camera_pos: [f32; 3], // xyz + time: f32, + is_light: [f32; 4], } impl egui_wgpu::CallbackTrait for GpuData { @@ -337,8 +409,13 @@ impl egui_wgpu::CallbackTrait for GpuData { render_pass.set_pipeline(&resources.pipeline); render_pass.set_bind_group(0, &resources.bind_group, &[]); render_pass.set_vertex_buffer(0, resources.vertex_buffer.slice(..)); + render_pass.set_vertex_buffer(1, resources.instance_buffer.slice(..)); render_pass.set_index_buffer(resources.index_buffer.slice(..), wgpu::IndexFormat::Uint16); - render_pass.draw_indexed(0..mesh::CUBE_INDICES.len() as u32, 0, 0..1); // 36 vertices for a cube (6 faces * 2 triangles * 3 vertices) + render_pass.draw_indexed( + 0..mesh::CUBE_INDICES.len() as u32, + 0, + 0..resources.instance_count, + ); } } @@ -347,6 +424,8 @@ struct CubeRenderResources { pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, uniform_buffer: wgpu::Buffer, + instance_buffer: wgpu::Buffer, vertex_buffer: wgpu::Buffer, index_buffer: wgpu::Buffer, + instance_count: u32, } diff --git a/crates/notedeck_dave/src/dave.wgsl b/crates/notedeck_dave/src/dave.wgsl index 2cd698e..306f9b8 100644 --- a/crates/notedeck_dave/src/dave.wgsl +++ b/crates/notedeck_dave/src/dave.wgsl @@ -1,8 +1,9 @@ - struct Uniforms { view_proj: mat4x4, model: mat4x4, - camera_pos: vec4, // world-space camera position + camera_pos: vec3, + time: f32, + is_light: vec4, }; @group(0) @binding(0) @@ -12,40 +13,85 @@ 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(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 world = uniforms.model * vec4(in_pos, 1.0); + + 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 = (model rotation) * in_normal + // 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(transpose(inverse(nmat)) * in_normal); out.normal = normalize(nmat * in_normal); out.world_pos = world.xyz; + out.color = color; return out; } -@fragment -fn fs_main_debug(in: VSOut) -> @location(0) vec4 { - let g = normalize(cross(dpdx(in.world_pos), dpdy(in.world_pos))); - let n = normalize(in.normal); - let shown = 0.5 * (n + vec3(1.0,1.0,1.0)); - return vec4(shown, 1.0); -} - @fragment fn fs_main(in: VSOut) -> @location(0) vec4 { - let material_color = vec3(1.0, 1.0, 1.0); + // 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; @@ -53,20 +99,57 @@ fn fs_main(in: VSOut) -> @location(0) vec4 { 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.xyz; - - let ambient = ambient_strength * light_color; + let view_pos = uniforms.camera_pos; let n = normalize(in.normal); let l = normalize(light_pos - in.world_pos); - let diff = max(dot(n, l), 0.0); - let diffuse = diffuse_strength * diff * light_color; - let v = normalize(view_pos - in.world_pos); let r = reflect(-l, n); - let spec = pow(max(dot(v, r), 0.0), shininess); - let specular = specular_strength * spec * light_color; - let result = (ambient + diffuse + specular) * material_color; - return vec4(result, 1.0); + 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)); } diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs index 20c880d..20270e2 100644 --- a/crates/notedeck_dave/src/lib.rs +++ b/crates/notedeck_dave/src/lib.rs @@ -27,7 +27,7 @@ pub use vec3::Vec3; mod avatar; mod config; -mod mesh; +pub(crate) mod mesh; mod messages; mod quaternion; mod tools; @@ -180,6 +180,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } fn ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse { + /* + let rect = ui.available_rect_before_wrap(); + if let Some(av) = self.avatar.as_mut() { + av.render(rect, ui); + ui.ctx().request_repaint(); + } + DaveResponse::default() + */ + DaveUi::new(self.model_config.trial, &self.chat, &mut self.input).ui( app_ctx, &mut self.jobs, diff --git a/crates/notedeck_dave/src/mesh.rs b/crates/notedeck_dave/src/mesh.rs index eda2874..7cea608 100644 --- a/crates/notedeck_dave/src/mesh.rs +++ b/crates/notedeck_dave/src/mesh.rs @@ -19,6 +19,29 @@ impl Vertex { }; } +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Instance { + pub base_pos: [f32; 3], + pub scale: f32, + pub seed: f32, + pub color: [f32; 3], +} + +impl Instance { + pub const ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ + 2 => Float32x3, // base_pos + 3 => Float32, // scale + 4 => Float32, // seed + 5 => Float32x3 // color + ]; + pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &Self::ATTRS, + }; +} + // 6 faces * 4 verts. Each face has a constant normal. #[rustfmt::skip] pub const CUBE_VERTICES: [Vertex; 24] = [