본문으로 건너뛰기
Cosmic Signature

소스 코드

Cosmic Signature 각 부분의 소스 코드와 각 시드를 작품으로 바꾸는 프로그램입니다.

프로젝트 저장소

  • 프론트엔드

    cosmicgame-frontend

    Next.js로 만든 웹사이트와 애플리케이션 인터페이스입니다.

  • Go 백엔드(Augur Explorer)

    augur-explorer

    온체인 이벤트를 인덱싱하고 API로 프로토콜 데이터를 제공합니다.

  • 스마트 컨트랙트

    Cosmic-Signature

    Arbitrum에서 Cosmic Signature 프로토콜을 정의하는 Solidity 컨트랙트입니다.

  • 이미지 생성

    CS-Image-Generation

    Cosmic Signature 이미지와 영상을 생성하는 Rust 기반 삼체 시뮬레이션과 렌더러입니다.

시드에서 이미지까지

아래 코드가 NFT의 시드를 단계별로 작품으로 바꾸는 과정입니다.

  1. 시드

    NFT의 시드로, 각인될 때 온체인에 기록됩니다.

  2. SHA3-256 무작위 스트림

    시드를 해시하여 재현 가능한 무작위 바이트 스트림을 만듭니다.

  3. 삼체 시뮬레이션

    스트림에서 세 천체의 질량, 위치, 속도를 정하고 궤도를 계산합니다.

  4. 스펙트럼 렌더러

    각 천체의 경로를 스펙트럼을 따라 조금씩 변하는 색으로 그립니다.

  5. 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");}

IPFS 콘텐츠 식별자ipfs://QmWEao…gyXTmipfs://QmWEao2HjCvyHJSbYnWLyZj8HfFardxzuNh7AUk1jgyXTm

IPFS (새 탭에서 열림)GitHub (새 탭에서 열림)