Browse Source

Moved to custom 3D renderer engine

pull/4/head
Stephen 5 months ago
parent
commit
aaf82d0ef2
8 changed files with 377 additions and 1519 deletions
  1. +140
    -1368
      Cargo.lock
  2. +1
    -2
      Cargo.toml
  3. +5
    -5
      README.md
  4. +23
    -69
      src/main.rs
  5. +202
    -0
      src/render.rs
  6. +2
    -1
      src/tests.rs
  7. +4
    -0
      src/vector3d.rs
  8. +0
    -74
      src/video_3d.rs

+ 140
- 1368
Cargo.lock
File diff suppressed because it is too large
View File


+ 1
- 2
Cargo.toml View File

@ -7,12 +7,11 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
kiss3d = "0.24.1"
nalgebra = "0.21"
rand = "0.7"
rayon = "1.3"
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3"
png = "0.16"
[target.x86_64-unknown-linux-gnu]
rustflags = [


+ 5
- 5
README.md View File

@ -4,17 +4,17 @@ A simple 3D gravitational simulator I wrote in Rust.
## Usage
First, generate a 3D video.
First, generate the frames.
`cargo run --release -- create`
This will generate a `video.3dv` file.
This will generate a bunch of png images in the `frames` directory.
Next, watch the video.
Next, create the video.
`cargo run --release -- watch`
`ffmpeg -i frames/%04d.png out.mp4`
This will load the `video.3dv` file, so that you can watch it.
The video will be created in the current directory with the name `out.mp4`.
This is very much a WIP, and all parameters are hardcoded for now.

+ 23
- 69
src/main.rs View File

@ -1,24 +1,20 @@
extern crate kiss3d;
extern crate nalgebra as na;
use na::{Vector3, Translation3, Point3};
use kiss3d::window::Window;
use kiss3d::scene::SceneNode;
use rand::prelude::*;
use rayon::prelude::*;
use std::env;
use render::Scene;
mod barnes_hut;
mod rigid_point;
mod video_3d;
mod vector3d;
mod render;
use rigid_point::RigidPoint;
use barnes_hut::Octree;
use vector3d::Vector3D;
use video_3d::*;
const G: f64 = 0.00667408;
#[derive(Clone)]
@ -46,12 +42,6 @@ impl Universe {
self.lines.push(Line {start: start, end: end});
}
fn render(&self, window: &mut Window) {
for line in &self.lines {
window.draw_line(&line.start, &line.end, &Point3::new(0.0, 1.0, 0.0));
}
}
//Not completely correct, since the velocity and position are at slightly different times
fn calc_total_energy(&self) -> f64 {
let mut energy: f64 = 0.0;
@ -71,7 +61,7 @@ impl Universe {
energy
}
fn combine(&mut self, i: usize, j: usize, window: &mut Window) {
fn combine(&mut self, i: usize, j: usize) {
let a = self.particles[i].clone();
let b = self.particles[j].clone();
@ -112,83 +102,47 @@ impl Universe {
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage: ./{} watch|create", args[0]);
println!("Usage: ./{} create", args[0]);
return;
}
match args[1].as_ref() {
"watch" => watch_video(),
"create" => create_video(),
"create" => create_frames(1000),
_ => {
println!("Usage: ./{} watch|create", args[0]);
println!("Usage: ./{} create", args[0]);
return;
}
}
}
fn watch_video() {
let vid = Video3D::load_file("video.3dv");
let mut window = Window::new("3DVideo viewer");
let mut i: u64 = 0;
let mut nodes: Vec<SceneNode> = Vec::new();
while window.render() {
vid.get_frame(i as usize % vid.frames.len()).spheres.into_iter().enumerate().for_each(|(j, sphere)| {
if j >= nodes.len() {
let mut node = window.add_sphere(sphere.radius as f32);
node.set_color(sphere.r as f32, sphere.g as f32, sphere.b as f32);
node.append_translation(&Translation3::new(sphere.x as f32, sphere.y as f32, sphere.z as f32));
nodes.push(node);
}
else {
// We can reuse existing spheres
nodes[j].set_color(sphere.r as f32, sphere.g as f32, sphere.b as f32);
nodes[j].set_local_translation(Translation3::new(sphere.x as f32, sphere.y as f32, sphere.z as f32));
}
});
// TODO destroy nodes that aren't being reused
i += 1;
}
}
fn create_video() {
/* ====== RENDER TO A 3D VIDEO ====== */
let mut vid = Video3D::new(60);
fn create_frames(num_iterations: usize) {
let delta = 0.005;
let mut universe = generate_random_points(delta);
for iteration in 0..100 {
println!("Start iter");
let mut last_percent = 0;
for iteration in 1..(num_iterations + 1) {
for _ in 0..10 {
tick(&mut universe);
calc_velocities(&mut universe, delta);
}
println!("Stepping complete");
//Create frame from universe
let mut frame = Frame3D::new();
for p in &universe.particles {
let pos = p.position;
frame.push_sphere(Sphere3D::new(pos.x, pos.y, pos.z,
p.radius,
1.0, 1.0, 1.0));
if iteration * 100 / num_iterations != last_percent {
last_percent = iteration * 100 / num_iterations;
println!("{}% complete", last_percent);
}
//Push frame to vid
vid.push_frame(frame);
//if iteration % 10 == 0 {
println!("{}% complete", iteration / 10);
//}
// Create frame
let mut s = Scene::new(1920, 1080, 90.0);
for p in &universe.particles {
s.add_point_to_scene(p.position, 2000.0);
}
s.save(&format!("frames/{:04}.png", iteration));
}
vid.save_file("video.3dv");
}
fn generate_random_points(delta: f64) -> Universe {
let mut ret: Universe = Universe::new(delta);
let mut rng = rand::thread_rng();
for i in 0..100_000 {
for i in 0..50_000 {
let x: f64 = rng.gen_range(-50.0, 50.0);
let y: f64 = rng.gen_range(-50.0, 50.0);
let z: f64 = rng.gen_range(-5.0, 5.0); // Flat galaxy
@ -208,7 +162,7 @@ fn generate_random_points(delta: f64) -> Universe {
fn calc_velocities(u: &mut Universe, delta: f64) {
//Find min/max
println!("Finding min/max");
// println!("Finding min/max");
let mut min: Vector3<f64> = Vector3::new(0.0, 0.0, 0.0);
let mut max: Vector3<f64> = Vector3::new(0.0, 0.0, 0.0);
for particle in &u.particles {
@ -237,20 +191,20 @@ fn calc_velocities(u: &mut Universe, delta: f64) {
}
}
println!("Constructing tree");
// println!("Constructing tree");
let p = u.particles.iter().filter(|x| {x.index != -1}).collect::<Vec<_>>();
let mut oct = Octree::new(Vector3D::from_na_vector(min),
Vector3D::from_na_vector(max), p.len());
oct.construct_tree(&p);
println!("Barnes hut");
// println!("Barnes hut");
u.particles.par_iter_mut().for_each(|mut p| {
oct.barnes_hut(&mut p, delta);
});
println!("Done");
// println!("Done");
}
fn tick(u: &mut Universe) {


+ 202
- 0
src/render.rs View File

@ -0,0 +1,202 @@
use crate::vector3d::Vector3D;
use std::path::Path;
use std::fs::File;
use std::io::BufWriter;
/*
struct Camera {
position: Vector3D,
direction: Vector3D,
fov_degrees_x: f64,
fov_degrees_y: f64,
width: u32, // pixels
height: u32, // pixels
corners: [Vector3D; 4]
}
impl Camera {
fn new(position: Vector3D, facing: Vector3D, fov: f64, width: u32, height: u32) -> Self {
let focal_len = width as f64 / fov.tan();
let center_vec = (position - facing).normalize() * focal_length;
Self {
}
}
}
*/
pub struct Scene {
framebuffer: Vec<f64>, // holds brightness values
width: u32,
height: u32,
// Camera stuff
fov_rad_x: f64,
fov_rad_y: f64,
focal_len: f64
}
impl Scene {
pub fn new(width: u32, height: u32, fov: f64) -> Self {
let fov = fov.to_radians() / 2.0;
let l = (width as usize) * (height as usize);
let mut framebuffer = Vec::with_capacity(l);
for _ in 0..l {
framebuffer.push(0.0);
}
let fov_rad_x = fov;
let fov_rad_y = (fov.tan() * (height as f64) / (width as f64)).atan();
let focal_len = (width as f64 / 2.0) / fov.tan();
Self {
framebuffer,
width,
height,
fov_rad_x,
fov_rad_y,
focal_len
}
}
pub fn add_point_to_scene(&mut self, position: Vector3D, intensity: f64) {
// Camera is offset by a z of 100
let mut position = position;
position.z += 100.0;
let dist_squared = position.dist_squared();
let hyp = dist_squared.sqrt();
if hyp < 0.1 || position.z < 0.0 { return; }
let angle_horizontal_rad = (position.x / hyp).asin();
let angle_vertical_rad = (position.y / hyp).asin();
if angle_horizontal_rad < -self.fov_rad_x || angle_horizontal_rad > self.fov_rad_x ||
angle_vertical_rad < -self.fov_rad_y || angle_vertical_rad > self.fov_rad_y {
// Out of range
return;
}
// Convert to (x, y) coordinate
let x = (self.width as f64 / 2.0 + self.focal_len * angle_horizontal_rad.tan()).round() as i64;
let y = (self.height as f64 / 2.0 + self.focal_len * angle_vertical_rad.tan()).round() as i64;
if x == self.width as i64 || y == self.height as i64 {
return; // Just barely out of bounds
}
assert!(x >= 0);
assert!(x < self.width as i64);
assert!(y >= 0);
assert!(y < self.height as i64);
let brightness_at_camera = intensity / dist_squared;
// println!("{}", brightness_at_camera);
self.framebuffer[(y as usize) * (self.width as usize) + x as usize] += brightness_at_camera;
}
pub fn save(&self, filename: &str) {
let mut framebuffer: Vec<u8> = Vec::with_capacity((self.width as usize) * (self.height as usize));
for p in &self.framebuffer {
let p = if *p == 0.0 { 0.0 } else { Self::sigmoid(*p) };
// when p == 0, h = 240
// when p == 1, h = 0
let h = 240.0 - (240.0 * p);
let (r, g, b) = Self::hsv_to_rgb(h, p, p);
framebuffer.push(r); // R
framebuffer.push(g); // G
framebuffer.push(b); // B
framebuffer.push(255); // A
};
let path = Path::new(filename);
let file = File::create(path).unwrap();
let ref mut w = BufWriter::new(file);
let mut encoder = png::Encoder::new(w, self.width, self.height);
encoder.set_color(png::ColorType::RGBA);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&framebuffer).unwrap(); // Save
}
fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (u8, u8, u8) {
let mut h = if h >= 360.0 { 0.0 } else { h };
h /= 60.0;
let i = h.floor() as u32;
let ff = h - i as f64;
let p = v * (1.0 - s);
let q = v * (1.0 - (s * ff));
let t = v * (1.0 - (s * (1.0 - ff)));
let v = (v * 255.0) as u8;
let p = (p * 255.0) as u8;
let q = (q * 255.0) as u8;
let t = (t * 255.0) as u8;
match i {
0 => {
(v, t, p)
}
1 => {
(q, v, p)
}
2 => {
(p, v, t)
}
3 => {
(p, q, v)
}
4 => {
(t, p, v)
}
5 => {
(v, p, q)
}
_ => panic!("Invalid hsv")
}
}
fn sigmoid(f: f64) -> f64 {
let x = f.exp();
x / (x + 1.0)
}
}
#[cfg(test)]
mod tests {
use crate::render::Scene;
use crate::vector3d::Vector3D;
#[test]
fn basic_tests() {
let mut s = Scene::new(1920, 1080, 90.0);
s.add_point_to_scene(Vector3D::new(0.0, 0.0, 0.0), 5.0);
s.add_point_to_scene(Vector3D::new(10.0, 0.0, 0.0), 10.0);
s.add_point_to_scene(Vector3D::new(0.0, 0.0, 10.0), 15.0);
s.add_point_to_scene(Vector3D::new(10.0, 0.0, 10.0), 20.0);
s.add_point_to_scene(Vector3D::new(0.0, -10.0, 0.0), 25.0);
assert!(s.framebuffer.iter().filter(|f| { f != &&0.0 }).next() == None);
s.add_point_to_scene(Vector3D::new(0.0, 10.0, 0.0), 25.0);
s.add_point_to_scene(Vector3D::new(0.0, 10.0, 0.0), 45.0);
assert_eq!(0.7, s.framebuffer[1920 * (1080 / 2) + (1920 / 2)]);
}
}

+ 2
- 1
src/tests.rs View File

@ -1,5 +1,5 @@
/*
#[cfg(test)]
use super::*;
// Quick and dirty
@ -37,3 +37,4 @@ fn test_octree_partitioning() {
assert_eq!(i, Octree::get_id_from_center(center, new_center));
}
}
*/

+ 4
- 0
src/vector3d.rs View File

@ -41,6 +41,10 @@ impl Vector3D {
pub fn dist(self) -> f64 {
self.dist_squared().sqrt()
}
pub fn normalize(self) -> Self {
self / self.dist()
}
}
impl Mul<f64> for Vector3D {


+ 0
- 74
src/video_3d.rs View File

@ -1,74 +0,0 @@
use serde::{Serialize, Deserialize};
use std::io::prelude::*;
use std::io::BufWriter;
use std::fs::File;
#[derive(Serialize, Deserialize, Debug)]
pub struct Video3D {
framerate: u32,
pub frames: Vec<Frame3D>
}
impl Video3D {
pub fn new(framerate: u32) -> Self {
Self { framerate, frames: Vec::new() }
}
pub fn push_frame(&mut self, f: Frame3D) {
self.frames.push(f);
}
pub fn save_file(&self, filename: &str) -> std::io::Result<()> {
let mut buffer = BufWriter::new(File::create(filename)?);
buffer.write_all(&bincode::serialize(self).unwrap())?;
buffer.flush()?;
Ok(())
}
pub fn load_file(filename: &str) -> Self {
let mut buffer = Vec::new();
File::open(filename).unwrap().read_to_end(&mut buffer).unwrap();
bincode::deserialize(&mut buffer).unwrap()
}
pub fn get_frame(&self, i: usize) -> Frame3D {
self.frames[i].clone()
}
}
// May add other types in the future
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Frame3D {
pub spheres: Vec<Sphere3D>
}
impl Frame3D {
pub fn new() -> Self {
Self { spheres: Vec::new() }
}
pub fn push_sphere(&mut self, s: Sphere3D) {
self.spheres.push(s);
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Sphere3D {
pub x: f64,
pub y: f64,
pub z: f64,
pub radius: f64,
// Range from 0.0 to 1.0
pub r: f64,
pub g: f64,
pub b: f64,
}
impl Sphere3D {
// TODO make this signature less ridiculous
pub fn new(x: f64, y: f64, z: f64, radius: f64, r: f64, g: f64, b: f64) -> Self {
Self { x, y, z, radius, r, g, b }
}
}

Loading…
Cancel
Save