├── .gitignore ├── demo.gif ├── Cargo.toml ├── README.md ├── LICENSE ├── Cargo.lock └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/quaternions/HEAD/demo.gif -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quaternions" 3 | version = "0.1.0" 4 | authors = ["rexim "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | sdl2 = "0.33.0" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple exercise to learn about Quaternions 2 | 3 | ![](./demo.gif) 4 | 5 | This code is the result of a Random One-off stream: https://www.twitch.tv/videos/597042442 6 | 7 | ## Quick Start 8 | 9 | ```console 10 | $ cargo run 11 | ``` 12 | 13 | ## References 14 | 15 | - https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Using_quaternion_as_rotations 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 © Alexey Kutepov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bitflags" 5 | version = "1.2.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 8 | 9 | [[package]] 10 | name = "cfg-if" 11 | version = "0.1.10" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 14 | 15 | [[package]] 16 | name = "lazy_static" 17 | version = "1.4.0" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 20 | 21 | [[package]] 22 | name = "libc" 23 | version = "0.2.69" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 26 | 27 | [[package]] 28 | name = "quaternions" 29 | version = "0.1.0" 30 | dependencies = [ 31 | "sdl2", 32 | ] 33 | 34 | [[package]] 35 | name = "sdl2" 36 | version = "0.33.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "1f74124048ea86b5cd50236b2443f6f57cf4625a8e8818009b4e50dbb8729a43" 39 | dependencies = [ 40 | "bitflags", 41 | "lazy_static", 42 | "libc", 43 | "sdl2-sys", 44 | ] 45 | 46 | [[package]] 47 | name = "sdl2-sys" 48 | version = "0.33.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "c2e1deb61ff274d29fb985017d4611d4004b113676eaa9c06754194caf82094e" 51 | dependencies = [ 52 | "cfg-if", 53 | "libc", 54 | ] 55 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use sdl2::pixels::Color; 2 | use sdl2::event::Event; 3 | use sdl2::rect::Point; 4 | use std::time::Duration; 5 | 6 | #[derive(Clone, Copy)] 7 | struct Quaternion { 8 | a: f64, 9 | b: f64, 10 | c: f64, 11 | d: f64, 12 | } 13 | 14 | impl Quaternion { 15 | fn from_v3([x, y, z]: [f64; 3]) -> Quaternion { 16 | Self { 17 | a: 0.0, 18 | b: x, 19 | c: y, 20 | d: z, 21 | } 22 | } 23 | 24 | fn to_v3(self) -> [f64; 3] { 25 | let Quaternion { a, b, c, d } = self; 26 | assert!(f64::abs(a) < 1e-6); 27 | [b, c, d] 28 | } 29 | 30 | fn rot([x, y, z]: [f64; 3], theta: f64) -> Quaternion { 31 | let l = f64::sqrt(x * x + y * y + z * z); 32 | let c = f64::cos(theta * 0.5); 33 | let s = f64::sin(theta * 0.5); 34 | Self { 35 | a: c, 36 | b: x / l * s, 37 | c: y / l * s, 38 | d: z / l * s 39 | } 40 | } 41 | 42 | fn product(self, 43 | Quaternion { a:a2, b: b2, c: c2, d: d2 }: Quaternion) -> Quaternion { 44 | let Quaternion { a:a1, b: b1, c: c1, d: d1 } = self; 45 | Self { 46 | a: a1 * a2 - b1 * b2 - c1 * c2 - d1 * d2, 47 | b: a1 * b2 + b1 * a2 + c1 * d2 - d1 * c2, 48 | c: a1 * c2 - b1 * d2 + c1 * a2 + d1 * b2, 49 | d: a1 * d2 + b1 * c2 - c1 * b2 + d1 * a2, 50 | } 51 | } 52 | 53 | fn recip(self) -> Quaternion { 54 | let Quaternion { a, b, c, d } = self; 55 | let q = a * a + b * b + c * c + d * d; 56 | Self { 57 | a: a / q, 58 | b: -b / q, 59 | c: -c / q, 60 | d: -d / q, 61 | } 62 | } 63 | } 64 | 65 | const VS: [[f64; 3]; 8] = [ 66 | [-1.0, 1.0, 1.0], // 0 67 | [ 1.0, 1.0, 1.0], // 1 68 | [ 1.0, 1.0, -1.0], // 2 69 | [-1.0, 1.0, -1.0], // 3 70 | 71 | [-1.0, -1.0, 1.0], // 4 72 | [ 1.0, -1.0, 1.0], // 5 73 | [ 1.0, -1.0, -1.0], // 6 74 | [-1.0, -1.0, -1.0], // 7 75 | ]; 76 | 77 | const LS: [[usize; 2]; 12] = [ 78 | // Top Side 79 | [0, 1], 80 | [1, 2], 81 | [2, 3], 82 | [3, 0], 83 | 84 | // Bottom side 85 | [4, 5], 86 | [5, 6], 87 | [6, 7], 88 | [7, 4], 89 | 90 | // Vertical 91 | [0, 4], 92 | [1, 5], 93 | [2, 6], 94 | [3, 7] 95 | ]; 96 | 97 | fn project([x, y, z]: [f64; 3], r: f64) -> [f64; 2] { 98 | return [x * r / z, y / z]; 99 | } 100 | 101 | fn to_screen([x0, y0]: [f64; 2], w: f64, h: f64) -> [f64; 2] { 102 | let half_w = w * 0.5; 103 | let half_h = h * 0.5; 104 | let x = x0 * half_w + half_w; 105 | let y = y0 * half_h + half_h; 106 | [x, y] 107 | } 108 | 109 | fn epic_rotate(p: [f64; 3], theta: f64) -> [f64; 3] { 110 | let pq = Quaternion::from_v3(p); 111 | let rotq = Quaternion::rot([0.0, 1.0, 0.0], theta) 112 | .product(Quaternion::rot([1.0, 0.0, 0.0], theta)); 113 | rotq.product(pq).product(rotq.recip()).to_v3() 114 | } 115 | 116 | #[allow(dead_code)] 117 | fn rotate_y([x0, y0, z0]: [f64; 3], theta: f64) -> [f64; 3] { 118 | let x1 = x0 * f64::cos(theta) + z0 * f64::sin(theta); 119 | let z1 = x0 * f64::sin(theta) - z0 * f64::cos(theta); 120 | return [x1, y0, z1]; 121 | } 122 | 123 | fn translate([x0, y0, z0]: [f64; 3], [x1, y1, z1]: [f64; 3]) -> [f64; 3] { 124 | return [x0 + x1, y0 + y1, z0 + z1]; 125 | } 126 | 127 | const DISTANCE: f64 = 3.0; 128 | const BACKGROUND: Color = Color::RGB(18, 18, 18); 129 | const FOREGROUND: Color = Color::RGB(255, 150, 150); 130 | 131 | fn main() -> Result<(), String> { 132 | let sdl_context = sdl2::init()?; 133 | let video_subsystem = sdl_context.video()?; 134 | let window = video_subsystem.window("Quaternions", 800, 600) 135 | .position_centered() 136 | .resizable() 137 | .build() 138 | .map_err(|e| e.to_string())?; 139 | 140 | let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?; 141 | 142 | let mut event_pump = sdl_context.event_pump()?; 143 | let mut rotation: f64 = 0.0; 144 | 145 | 'running: loop { 146 | for event in event_pump.poll_iter() { 147 | match event { 148 | Event::Quit {..} => { 149 | break 'running 150 | }, 151 | _ => {} 152 | } 153 | 154 | } 155 | 156 | canvas.set_draw_color(BACKGROUND); 157 | canvas.clear(); 158 | 159 | canvas.set_draw_color(FOREGROUND); 160 | let (w, h) = canvas.window().size(); 161 | for [l1, l2] in &LS { 162 | let r = h as f64 / w as f64; 163 | let [sx0, sy0] = to_screen(project(translate(epic_rotate(VS[*l1], rotation), [0.0, 0.0, DISTANCE]), r), 164 | w as f64, h as f64); 165 | let [sx1, sy1] = to_screen(project(translate(epic_rotate(VS[*l2], rotation), [0.0, 0.0, DISTANCE]), r), 166 | w as f64, h as f64); 167 | canvas.draw_line(Point::new(sx0 as i32, sy0 as i32), 168 | Point::new(sx1 as i32, sy1 as i32))?; 169 | } 170 | 171 | canvas.present(); 172 | 173 | const FPS: u32 = 30; 174 | let dt = 1.0 / FPS as f64; 175 | ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / FPS)); 176 | rotation += 2.0 * dt; 177 | 178 | } 179 | 180 | Ok(()) 181 | } 182 | --------------------------------------------------------------------------------