Вихідний код
Вихідний код кожної частини Cosmic Signature і програма, яка перетворює кожен сід на твір.
Репозиторії проєкту
Фронтенд
cosmicgame-frontend
Вебсайт та інтерфейс застосунку на Next.js.
Бекенд на Go (Augur Explorer)
augur-explorer
Індексує ончейн-події та надає дані протоколу через API.
Смарт-контракти
Cosmic-Signature
Контракти на Solidity, що визначають протокол Cosmic Signature в мережі Arbitrum.
Генерація зображень
CS-Image-Generation
Симуляція задачі трьох тіл і рендерер на Rust для зображень та відео Cosmic Signature.
Від сіду до зображення
Як наведений нижче код крок за кроком перетворює сід NFT на твір.
Сід
Сід NFT, записаний у блокчейні під час закарбування.
Випадковий потік SHA3-256
Хешує сід у відтворюваний потік випадкових байтів.
Симуляція трьох тіл
Бере з потоку маси, положення й швидкості трьох тіл і обчислює їхні орбіти.
Спектральний рендерер
Малює шлях кожного тіла кольором, що поступово зсувається спектром.
Зображення Cosmic Signature NFT
Останній кадр стає зображенням, а всі кадри разом — відео. Той самий сід завжди дає той самий результат.
Переглядач коду
Програма на Rust, яка перетворює кожен сід на зображення й відео. Той самий файл опубліковано на IPFS.
Rust534 рядки
extern crate nalgebra as na;use na::{Vector3};use rayon::prelude::*;use sha3::{Digest, Sha3_256};pub struct Sha3RandomByteStream { hasher: Sha3_256, seed: Vec<u8>, buffer: Vec<u8>, index: usize,}impl Sha3RandomByteStream { pub fn new(seed: &Vec<u8>) -> Self { let mut hasher = Sha3_256::new(); let cloned_seed = seed.clone(); hasher.update(seed); let buffer = hasher.clone().finalize_reset().to_vec(); Self { hasher, seed: cloned_seed, buffer, index: 0, } } pub fn next_byte(&mut self) -> u8 { if self.index >= self.buffer.len() { self.hasher.update(&self.seed); self.hasher.update(&self.buffer); self.buffer = self.hasher.finalize_reset().to_vec(); self.index = 0; } let byte = self.buffer[self.index]; self.index += 1; byte } pub fn next_u64(&mut self) -> u64 { let mut bytes = [0u8; 8]; for i in 0..8 { bytes[i] = self.next_byte(); } u64::from_le_bytes(bytes) } pub fn next_f64(&mut self) -> f64 { let value: u64 = self.next_u64(); let max_value = u64::MAX; (value as f64) / (max_value as f64) } pub fn gen_range(&mut self, min: f64, max: f64) -> f64 { let num = self.next_f64(); let range = max - min; let value = num * range + min; value } pub fn random_mass(&mut self) -> f64 { self.gen_range(100.0, 300.0) } pub fn random_location(&mut self) -> f64 { let n = 1500.0; self.gen_range(-n, n) } pub fn random_velocity(&mut self) -> f64 { let n: f64 = 25.0; self.gen_range(-n, n) }}#[derive(Clone)]struct Body { mass: f64, position: Vector3<f64>, velocity: Vector3<f64>, acceleration: Vector3<f64>,}const G: f64 = 9.8;impl Body { fn new(mass: f64, position: Vector3<f64>, velocity: Vector3<f64>) -> Body { Body { mass, position, velocity, acceleration: Vector3::zeros(), } } fn update_acceleration(&mut self, other_mass: f64, other_position: Vector3<f64>) { let dir: Vector3<f64> = self.position - other_position; let mag = dir.norm(); self.acceleration += -G * other_mass * dir / (mag * mag * mag); } fn reset_acceleration(&mut self) { self.acceleration = Vector3::zeros(); }}fn verlet_step(bodies: &mut [Body], dt: f64) { for i in 0..bodies.len() { bodies[i].reset_acceleration(); for j in 0..bodies.len() { if i != j { bodies[i].update_acceleration(bodies[j].mass, bodies[j].position); } } } for i in 0..bodies.len() { bodies[i].position = bodies[i].position + bodies[i].velocity * dt + 0.5 * bodies[i].acceleration * (dt * dt); } for i in 0..bodies.len() { for j in 0..bodies.len() { if i != j { bodies[i].update_acceleration(bodies[j].mass, bodies[j].position); } } } for i in 0..bodies.len() { bodies[i].velocity = bodies[i].velocity + 0.5 * bodies[i].acceleration * dt; }}use std::f64::{INFINITY, NEG_INFINITY};use image::{ImageBuffer, Rgb};use imageproc::drawing::draw_filled_circle_mut;use palette::{FromColor, Hsl, Srgb};fn get_single_color_walk(rng: &mut Sha3RandomByteStream, len: usize) -> Vec<Rgb<u8>> { let mut colors = Vec::new(); let mut hue = rng.gen_range(0.0, 360.0); for _ in 0..len { if rng.next_byte() & 1 == 0 { hue += 0.1; } else { hue -= 0.1; } if hue < 0.0 { hue += 360.0; } if hue > 360.0 { hue -= 360.0; } let hsl = Hsl::new(hue, 1.0, 0.5); let my_new_rgb = Srgb::from_color(hsl); let r = (my_new_rgb.red * 255.0) as u8; let g = (my_new_rgb.green * 255.0) as u8; let b = (my_new_rgb.blue * 255.0) as u8; let line_color: Rgb<u8> = Rgb([r, g, b]); colors.push(line_color); } colors}fn get_3_colors(rng: &mut Sha3RandomByteStream, len: usize) -> Vec<Vec<Rgb<u8>>> { let mut colors = Vec::new(); for _ in 0..3 { let c = get_single_color_walk(rng, len); colors.push(c); } colors}fn plot_positions( positions: &Vec<Vec<Vector3<f64>>>, frame_size: u32, snake_len: f64, init_len: usize, hide: &Vec<bool>, colors: &Vec<Vec<Rgb<u8>>>, frame_interval: usize) -> Vec<ImageBuffer<Rgb<u8>, Vec<u8>>> { let background_color = Rgb([0u8, 0u8, 0u8]); // Find the minimum and maximum coordinates for x and y let (mut min_x, mut min_y) = (INFINITY, INFINITY); let (mut max_x, mut max_y) = (NEG_INFINITY, NEG_INFINITY); let white_color = Rgb([255, 255, 255]); for body_idx in 0..positions.len() { if hide[body_idx] { continue; } for step in 0..positions[body_idx].len() { let x = positions[body_idx][step][0]; let y = positions[body_idx][step][1]; if x < min_x { min_x = x; } if y < min_y { min_y = y; } if x > max_x { max_x = x; } if y > max_y { max_y = y; } } } // TODO: make sure that scaling is equal in both directions let x_range = max_x - min_x; let y_range = max_y - min_y; let x_center = (min_x + max_x) / 2.0; let y_center = (min_y + max_y) / 2.0; let mut range = if x_range > y_range { x_range } else { y_range }; min_x = x_center - (range / 2.0) * 1.1; max_x = x_center + (range / 2.0) * 1.1; min_y = y_center - (range / 2.0) * 1.1; range = max_x - min_x; let mut frames = Vec::new(); let mut snake_end: usize = frame_interval; loop { let mut img = ImageBuffer::from_fn(frame_size, frame_size, |_, _| background_color); let mut snake_starts: [usize; 3] = [0, 0, 0]; for body_idx in 0..positions.len() { if hide[body_idx] { continue; } let mut total_dist: f64 = 0.0; let mut idx = snake_end; loop { if idx <= 1 || total_dist > range * snake_len { break; } let x1 = positions[body_idx][idx][0]; let y1 = positions[body_idx][idx][1]; let x2 = positions[body_idx][idx - 1][0]; let y2 = positions[body_idx][idx - 1][1]; let dist = ((x1 - x2).powi(2) + (y1 - y2).powi(2)).sqrt(); total_dist += dist; idx -= 1; } snake_starts[body_idx] = idx; for i in snake_starts[body_idx]..snake_end { let x1 = positions[body_idx][i][0]; let y1 = positions[body_idx][i][1]; // Scale and shift positions to fit within the image dimensions let x1p = (((x1 - min_x) / range) * (frame_size as f64)).round(); let y1p = (((y1 - min_y) / range) * (frame_size as f64)).round(); draw_filled_circle_mut(&mut img, (x1p as i32, y1p as i32), 6, colors[body_idx][i]); //draw_filled_circle_mut(&mut img, (x1p as i32, y1p as i32), 6, white_color); } } let mut blurred_img = imageproc::filter::gaussian_blur_f32(&img, 6.0); for body_idx in 0..positions.len() { if hide[body_idx] { continue; } for i in snake_starts[body_idx]..snake_end { let x1 = positions[body_idx][i][0]; let y1 = positions[body_idx][i][1]; // Scale and shift positions to fit within the image dimensions let x1p = ((x1 - min_x) / range * (frame_size as f64 - 1.0)).round(); let y1p = ((y1 - min_y) / range * (frame_size as f64 - 1.0)).round(); draw_filled_circle_mut(&mut blurred_img, (x1p as i32, y1p as i32), 1, white_color); } } if snake_end >= init_len { frames.push(imageproc::filter::gaussian_blur_f32(&blurred_img, 1.0)); } snake_end += frame_interval; if snake_end >= positions[0].len() { break; } } return frames;}extern crate rustfft;use rustfft::FftPlanner;use rustfft::num_complex::Complex;fn fourier_transform(input: &[f64]) -> Vec<Complex<f64>> { let n = input.len(); // Create an FFT planner let mut planner = FftPlanner::new(); let fft = planner.plan_fft_forward(n); // Create complex input let mut complex_input: Vec<Complex<f64>> = input .iter() .map(|&val| Complex::new(val, 0.0)) .collect(); // Perform the FFT fft.process(&mut complex_input); complex_input}use statrs::statistics::Statistics;fn non_chaoticness(m1: f64, m2: f64, m3: f64, positions: &Vec<Vec<Vector3<f64>>>) -> f64 { let mut r1: Vec<f64> = vec![0.0; positions[0].len()]; let mut r2: Vec<f64> = vec![0.0; positions[0].len()]; let mut r3: Vec<f64> = vec![0.0; positions[0].len()]; for i in 0..positions[0].len() { let p1 = positions[0][i]; let p2 = positions[1][i]; let p3 = positions[2][i]; let center_of_mass1 = (m2 * p2 + m3 * p3) / (m2 + m3); let center_of_mass2 = (m1 * p1 + m3 * p3) / (m1 + m3); let center_of_mass3 = (m2 * p2 + m1 * p1) / (m2 + m1); let dist1 = p1 - center_of_mass1; let dist2 = p2 - center_of_mass2; let dist3 = p3 - center_of_mass3; r1[i] = dist1.norm(); r2[i] = dist2.norm(); r3[i] = dist3.norm(); } let result1 = fourier_transform(&r1); let result2 = fourier_transform(&r2); let result3 = fourier_transform(&r3); let absolute1: Vec<f64> = result1 .iter() .map(|&val| (val.norm())) .collect(); let absolute2: Vec<f64> = result2 .iter() .map(|&val| (val.norm())) .collect(); let absolute3: Vec<f64> = result3 .iter() .map(|&val| (val.norm())) .collect(); let final_result1 = absolute1.std_dev().sqrt(); let final_result2 = absolute2.std_dev().sqrt(); let final_result3 = absolute3.std_dev().sqrt(); (final_result1 + final_result2 + final_result3) / 3.0}use std::io::Write;use std::process::{Command, Stdio};use image::DynamicImage;fn create_video_from_frames_in_memory(frames: &[ImageBuffer<Rgb<u8>, Vec<u8>>], output_file: &str, frame_rate: u32) { let mut command = Command::new("ffmpeg"); command .arg("-y") // Overwrite the output file if it exists .arg("-f") .arg("image2pipe") .arg("-vcodec") .arg("png") .arg("-r") .arg(frame_rate.to_string()) .arg("-i") .arg("-") .arg("-c:v") .arg("libx264") .arg("-pix_fmt") .arg("yuv420p") .arg(output_file) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut ffmpeg = command.spawn().expect("Failed to spawn ffmpeg process"); let ffmpeg_stdin = ffmpeg.stdin.as_mut().expect("Failed to open ffmpeg stdin"); for frame in frames { let dyn_image = DynamicImage::ImageRgb8(frame.clone()); dyn_image .write_to(ffmpeg_stdin, image::ImageOutputFormat::Png) .expect("Failed to write frame to ffmpeg stdin"); } ffmpeg_stdin.flush().expect("Failed to flush ffmpeg stdin"); drop(ffmpeg_stdin); // Close stdin to signal EOF to ffmpeg let output = ffmpeg.wait_with_output().expect("Failed to wait on ffmpeg process"); if !output.status.success() { eprintln!( "ffmpeg exited with an error: {}", String::from_utf8_lossy(&output.stderr) ); }}fn get_positions(mut bodies: Vec<Body>, num_steps: usize) -> Vec<Vec<Vector3<f64>>> { let dt = 0.001; let mut positions = vec![vec![Vector3::zeros(); num_steps]; bodies.len()]; for step in 0..num_steps { for (i, body) in bodies.iter().enumerate() { positions[i][step] = body.position; } verlet_step(&mut bodies, dt); } positions}fn get_best(rng: &mut Sha3RandomByteStream, num_iters: usize, num_steps_sim: usize, num_steps_video: usize) -> Vec<Vec<Vector3<f64>>> { let mut many_bodies: Vec<Vec<Body>> = vec![]; for _ in 0..num_iters { let body1 = Body::new(rng.random_mass(), Vector3::new(rng.random_location(), rng.random_location(), 0.0), Vector3::new(0.0, 0.0, rng.random_velocity())); let body2 = Body::new(rng.random_mass(), Vector3::new(rng.random_location(), rng.random_location(), 0.0), Vector3::new(0.0, 0.0, rng.random_velocity())); let body3 = Body::new(rng.random_mass(), Vector3::new(rng.random_location(), rng.random_location(), 0.0), Vector3::new(0.0, 0.0, rng.random_velocity())); let bodies = vec![body1, body2, body3]; many_bodies.push(bodies); } let mut results_par = vec![0.0; many_bodies.len()]; many_bodies.par_iter().map(|bodies| { let m1 = bodies[0].mass; let m2 = bodies[1].mass; let m3 = bodies[2].mass; let positions = get_positions(bodies.clone() , num_steps_sim); non_chaoticness(m1, m2, m3, &positions) }).collect_into_vec(&mut results_par); let mut best_idx = 0; let mut best_result = f64::INFINITY; for (i, &res) in results_par.iter().enumerate() { if res < best_result { best_result = res; best_idx = i; } } let bodies = &many_bodies[best_idx]; println!("mass: {} {} {} pos: {} {} | {} {} | {} {}", bodies[0].mass, bodies[1].mass, bodies[2].mass, bodies[0].position[0], bodies[0].position[1], bodies[1].position[0], bodies[1].position[1], bodies[2].position[0], bodies[2].position[1]); get_positions(bodies.clone(), num_steps_video)}use clap::Parser;/// Simple program to greet a person#[derive(Parser, Debug)]#[command(author, version, about, long_about = None)]struct Args { #[arg(long, default_value = "00")] seed: String, #[arg(long, default_value = "output")] file_name: String,}use hex;fn main() { let args = Args::parse(); let string_seed = if args.seed.starts_with("0x") { args.seed[2..].to_string() } else { args.seed.to_string() }; let seed = hex::decode(string_seed).expect("Invalid hexadecimal string"); let mut byte_stream = Sha3RandomByteStream::new(&seed); let steps = byte_stream.gen_range(400_000.0, 500_000.0) as usize; const NUM_TRIES: usize = 1_000; let positions = get_best(&mut byte_stream, NUM_TRIES, steps, steps); let colors = get_3_colors(&mut byte_stream, steps); let s: &str = args.file_name.as_str(); let file_name = format!("{}.mp4", s); println!("done simulating"); let snake_len = byte_stream.gen_range(0.2, 2.0); let init_len: usize = 0; let hide_2 = true; let hide_3 = true; let hide = vec![false, hide_2, hide_3]; const NUM_SECONDS: usize = 15; let target_length = 60 * NUM_SECONDS; let steps_per_frame: usize = steps / target_length; const FRAME_SIZE: u32 = 800; let frames = plot_positions(&positions, FRAME_SIZE, snake_len, init_len, &hide, &colors, steps_per_frame); let last_frame = frames[frames.len() - 1].clone(); if let Err(e) = last_frame.save(format!("{}.png", s)) { eprintln!("Error saving image: {:?}", e); } else { println!("Image saved successfully."); } create_video_from_frames_in_memory(&frames, &file_name, 60); println!("done creating video");}Ідентифікатор вмісту IPFSipfs://QmWEao…gyXTmipfs://QmWEao2HjCvyHJSbYnWLyZj8HfFardxzuNh7AUk1jgyXTm
IPFS (відкриється в новій вкладці)GitHub (відкриється в новій вкладці)