├── strange_attractors ├── __init__.py ├── glm │ ├── __init__.py │ ├── len.py │ ├── dot.py │ ├── cross.py │ └── normalize.py ├── burgers.py ├── lorenz.py └── utils.py ├── images ├── cover.jpg ├── burgers.jpg └── lorenz.jpg ├── license.md ├── .gitignore └── readme.md /strange_attractors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strange_attractors/glm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alaingalvan/strange-attractors/HEAD/images/cover.jpg -------------------------------------------------------------------------------- /images/burgers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alaingalvan/strange-attractors/HEAD/images/burgers.jpg -------------------------------------------------------------------------------- /images/lorenz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alaingalvan/strange-attractors/HEAD/images/lorenz.jpg -------------------------------------------------------------------------------- /strange_attractors/glm/len.py: -------------------------------------------------------------------------------- 1 | def sqdist(v): 2 | s = 0 3 | for i in range(0, len(v)): 4 | s += v[i] * v[i] 5 | 6 | return s -------------------------------------------------------------------------------- /strange_attractors/glm/dot.py: -------------------------------------------------------------------------------- 1 | def dot(u, v): 2 | """ 3 | Dot product of 2 N dimensional vectors. 4 | """ 5 | s = 0 6 | for i in range(0, len(u)): 7 | s += u[i] * v[i] 8 | 9 | return s -------------------------------------------------------------------------------- /strange_attractors/glm/cross.py: -------------------------------------------------------------------------------- 1 | def cross(u, v): 2 | """ 3 | return the cross product of 2 vectors. 4 | """ 5 | dim = len(u) 6 | s = [] 7 | for i in range(0, dim): 8 | if i == 0: 9 | j,k = 1,2 10 | s.append(u[j]*v[k] - u[k]*v[j]) 11 | elif i == 1: 12 | j,k = 2,0 13 | s.append(u[j]*v[k] - u[k]*v[j]) 14 | else: 15 | j,k = 0,1 16 | s.append(u[j]*v[k] - u[k]*v[j]) 17 | return s -------------------------------------------------------------------------------- /strange_attractors/glm/normalize.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | # 0 Threshold 4 | __EPSILON__ = 0.001 5 | 6 | 7 | def normalize(vec): 8 | """ 9 | Normalizes a n dimensional vector 10 | """ 11 | d = 0 12 | for i in range(0, len(vec)): 13 | d += vec[i] * vec[i] 14 | 15 | if d < __EPSILON__: 16 | return vec 17 | else: 18 | d = sqrt(d) 19 | 20 | for j in range(0, len(vec)): 21 | vec[j] /= d 22 | 23 | return vec 24 | -------------------------------------------------------------------------------- /strange_attractors/burgers.py: -------------------------------------------------------------------------------- 1 | def burgers(num_points=2000, start=[0.5, 0.5, 0.5], factor=250, delta=0.1, a=0.7, b=0.78): 2 | """ 3 | Generates Burger strange attractor. 4 | """ 5 | 6 | verts = [] 7 | zp = 0 8 | zq = 0 9 | xa = start[0] 10 | y = start[1] 11 | for i in range(0, num_points): 12 | x = (1.0 - a) * xa - y * y 13 | y = (1.0 + b) * y + xa * y 14 | verts.extend([ 15 | x * factor - radius, y * factor - radius, 0, 16 | x * factor - radius, y * factor + radius, 0, 17 | x * factor + radius, y * factor + radius, 0, 18 | x * factor + radius, y * factor - radius, 0 19 | ]) 20 | xa = x 21 | return verts 22 | -------------------------------------------------------------------------------- /strange_attractors/lorenz.py: -------------------------------------------------------------------------------- 1 | def lorenz(num_points=15000, start=[0.10, 0.10, 0.10], scaling_factor=20, delta=0.008, a=0.1, b=4.0, c=14.0, d=0.08): 2 | """ 3 | Generates Lorenz strange attractor. 4 | """ 5 | verts = [] 6 | x = start[0] 7 | y = start[1] 8 | z = start[2] 9 | 10 | for i in range(0, num_points): 11 | 12 | # calculate delta values 13 | dx = -a * x + (y * y) - (z * z) + a * c 14 | dy = x * (y - b * z) + d 15 | dz = z + x * (b * y + z) 16 | 17 | # calculate new coordinates 18 | x = x + delta * dx 19 | y = y + delta * dy 20 | z = z + delta * dz 21 | 22 | # scale and add them to the list 23 | verts.extend([ 24 | x * scaling_factor, z * scaling_factor, y * scaling_factor 25 | ]) 26 | 27 | return verts 28 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 4 | 5 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | For more information, please refer to -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | .vscode 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Strange Attractor Cover](images/cover.jpg) 2 | 3 | # Strange Attractors 4 | 5 | [![Download][download-img]][download-url] 6 | [![License][license-img]][license-url] 7 | 8 | Algorithms to generate strange attractors. Use these algorithms to create **Vertex Buffer Objects** and **Index Buffer Objects** for your graphics API of choice. 9 | 10 | ## Usage 11 | 12 | Each attractor function returns an array of vertex points in the format `[x1, y1, z1, x2, y2, z2, ...]`, from there you can use the included `spline_mesh` function to convert that data to a Vertex Buffer and Index Buffer: 13 | 14 | ```py 15 | from strange_attractors import burgers, spline_mesh 16 | 17 | (triangles, vertices, normals) = spline_mesh(burgers(), 4, 0.05) 18 | ``` 19 | 20 | ## API 21 | 22 | ### Attractors 23 | 24 | #### Lorenz 25 | 26 | ![Lorenz](images/lorenz.jpg) 27 | 28 | ```py 29 | from strange_attractors import lorenz 30 | 31 | lorenz(num_points=15000, start=[0.10, 0.10, 0.10], factor=20, delta=0.008, a=0.1, b=4.0, c=14.0, d=0.08): 32 | ``` 33 | 34 | | Arguments | Type | Description | 35 | |-----------|------|-------------| 36 | | `num_points`| `long` | Number of points | 37 | | `start`| `float[3]` | Starting vector | 38 | | `factor`| `long` | Scaling factor | 39 | | `delta`| `float` | Change over time | 40 | | `a`| `float` | A constant | 41 | | `b`| `float` | B constant | 42 | | `c`| `float` | C constant | 43 | | `d`| `float` | D constant | 44 | 45 | **Returns:** `float[3*num_points]` 46 | 47 | A twisting shape that looks almost like a tornado. 48 | 49 | #### Burgers 50 | 51 | ![Burgers](images/burgers.jpg) 52 | 53 | ```py 54 | from strange_attractors import burgers 55 | 56 | burgers(num_points=10000, start=[0.5, 0.5, 0.5], factor=250, delta=0.1, a=0.7, b=0.78) 57 | ``` 58 | 59 | | Arguments | Type | Description | 60 | |-----------|------|-------------| 61 | | `num_points`| `long` | Number of points | 62 | | `start`| `float[3]` | Starting vector | 63 | | `factor`| `long` | Scaling factor | 64 | | `delta`| `float` | Change over time | 65 | | `a`| `float` | A constant | 66 | | `b`| `float` | B constant | 67 | 68 | **Returns:** `float[3*num_points]` 69 | 70 | A round loopy shape, which looks a lot like a burger (hence the name). 71 | 72 | ### Utilities 73 | 74 | #### Spline Mesh 75 | 76 | ```py 77 | from strange_attractors import spline_mesh 78 | 79 | spline_mesh(points=[], resolution=4, radius=0.1) 80 | ``` 81 | 82 | | Arguments | Type | Description | 83 | |-----------|------|-------------| 84 | | `points`| `float[3*N]` | Input points | 85 | | `resolution`| `unsigned long` | Resolution of spline rings | 86 | | `radius`| `float` | Radius of spline | 87 | 88 | **Returns:** `(long[3*N], float[3*N], float[3*N])` 89 | 90 | Generates a VBO/IBO for a given spline. 91 | 92 | [license-img]: http://img.shields.io/:license-unlicense-blue.svg?style=flat-square 93 | [license-url]: http://unlicense.org/ 94 | [download-img]: http://img.shields.io/:download-🡣-gray.svg?style=flat-square 95 | [download-url]: https://github.com/alaingalvan/strange-attractors/archive/master.zip -------------------------------------------------------------------------------- /strange_attractors/utils.py: -------------------------------------------------------------------------------- 1 | from math import cos, sin, pi, floor, sqrt 2 | from .glm.normalize import normalize 3 | from .glm.dot import dot 4 | from .glm.cross import cross 5 | from .glm.len import sqdist 6 | 7 | 8 | def __qmul__(p, q): 9 | """ 10 | Quaternion multiplication. 11 | """ 12 | # p.scalar * q.vector 13 | q_s = [p[3] * q[0], p[3] * q[1], p[3] * q[2]] 14 | # q.scalar * p.vector 15 | p_s = [q[3] * p[0], q[3] * p[1], q[3] * p[2]] 16 | # cross(p.vector, q.vector) 17 | c = cross([p[0], p[1], p[2]], [q[0], q[1], q[2]]) 18 | 19 | return [ 20 | q_s[0] + p_s[0] + c[0], 21 | q_s[1] + p_s[1] + c[1], 22 | q_s[2] + p_s[2] + c[2], 23 | p[3] * q[3] - dot([p[0], p[1], p[2]], [q[0], q[1], q[2]]) 24 | ] 25 | 26 | 27 | def __rotate__(point, vec_from, vec_to): 28 | """ 29 | Rotates a point from 1 vector to another. 30 | """ 31 | # Normalized vectors 32 | n_from = normalize(vec_from) 33 | n_to = normalize(vec_to) 34 | dt = dot(n_from, n_to) 35 | c = cross(n_from, n_to) 36 | 37 | # Convert point to quaternion 38 | q_point = [point[0], point[1], point[2], 0] 39 | 40 | # Rotation Quaternion 41 | q = [0, 0, 0, 1] 42 | 43 | if dt < 0.9999 and dt > -.9999: 44 | q = [ 45 | c[0], 46 | c[1], 47 | c[2], 48 | dt + 1 49 | ] 50 | 51 | # Normalize q 52 | q_len = sqrt(sqdist(q)) 53 | for i in range(0, len(q)): 54 | q[i] /= q_len 55 | else: 56 | return point 57 | 58 | # Get inverse/conj of q 59 | q_conj = [-q[0], -q[1], -q[2], q[3]] 60 | 61 | # Premultiply by q 62 | q_point = __qmul__(q, q_point) 63 | 64 | # Postmultiply by inverse of q 65 | q_point = __qmul__(q_point, q_conj) 66 | 67 | # Extract point 68 | return [q_point[0], q_point[1], q_point[2]] 69 | 70 | 71 | def __gen_circle__(point=[0, 0, 0], resolution=4, radius=0.1, normal=[1, 0, 0]): 72 | """ 73 | Generate a circle around a point acording to a given normal. 74 | 75 | """ 76 | verts = [] 77 | normals = [] 78 | 79 | for i in range(0, resolution): 80 | 81 | # Generate point with circle normal on +x axis 82 | angle = i / resolution 83 | xx = cos(angle * 2 * pi) 84 | yy = sin(angle * 2 * pi) 85 | cur_vert = [xx * radius, yy * radius, 0] 86 | cur_norm = [xx, yy, 0] 87 | 88 | # Multiply by transform matrix based on normal 89 | cur_vert = __rotate__(cur_vert, [0, 0, 1], normal) 90 | cur_norm = __rotate__(cur_norm, [0, 0, 1], normal) 91 | 92 | # Add translation 93 | cur_vert[0] += point[0] 94 | cur_vert[1] += point[1] 95 | cur_vert[2] += point[2] 96 | 97 | # And add to VBO 98 | verts.extend(cur_vert) 99 | normals.extend(cur_norm) 100 | 101 | return (verts, normals) 102 | 103 | 104 | def __spline__(points=[], resolution=12): 105 | """ 106 | Converts a vertex point list to a high resolution spline. 107 | """ 108 | s_points = [] 109 | for p in range(0, floor(len(points) / (3 * 2))): 110 | # @TODO Interpoliate prev and next 111 | 112 | if 3 * (p + 1) < len(points): 113 | cur_point = [ 114 | points[3 * p], 115 | points[3 * p + 1], 116 | points[3 * p + 2] 117 | ] 118 | 119 | next_point = [ 120 | points[3 * (p + 1)], 121 | points[3 * (p + 1) + 1], 122 | points[3 * (p + 1) + 2] 123 | ] 124 | 125 | s_points.extend(cur_point) 126 | for i in range(0, resolution): 127 | ratio = i / resolution 128 | # @TODO - Figure out interpoliation between cur and next for spline 129 | 130 | return s_points 131 | 132 | 133 | def spline_mesh(points=[], resolution=4, radius=0.1): 134 | """ 135 | Generates a VBO/IBO for a given spline. 136 | 137 | Parameters 138 | ---------- 139 | points : list of long 140 | A list of triples, [x1, y1, z1, x2, y2, z2, ...`] 141 | 142 | resolution: unsigned long 143 | The resolution of the spline circle (4 means square) 144 | 145 | radius: float 146 | The radius of the spline circle 147 | """ 148 | 149 | # Initial vars 150 | vertices = [] 151 | triangles = [] 152 | normals = [] 153 | num_points = floor(len(points) / 3) 154 | 155 | for i in range(0, num_points): 156 | cur_point = [ 157 | points[3 * i], 158 | points[3 * i + 1], 159 | points[3 * i + 2] 160 | ] 161 | normal = [] 162 | 163 | if (i < num_points - 1): 164 | normal = [ 165 | cur_point[0] - points[3 * (i + 1)], 166 | cur_point[1] - points[3 * (i + 1) + 1], 167 | cur_point[2] - points[3 * (i + 1) + 2] 168 | ] 169 | else: 170 | normal = [ 171 | cur_point[0] - points[3 * (i - 1)], 172 | cur_point[0] - points[3 * (i - 1) + 1], 173 | cur_point[0] - points[3 * (i - 1) + 2] 174 | ] 175 | 176 | # ➡️ normalize normal vector 177 | normal = normalize(normal) 178 | 179 | (circle_verts, circle_norms) = __gen_circle__( 180 | cur_point, resolution, radius, normal) 181 | 182 | vertices.extend(circle_verts) 183 | normals.extend(circle_norms) 184 | 185 | if (i > 1): 186 | for c in range(0, resolution): 187 | # Middle Rings 188 | prev_row = resolution * (i - 1) 189 | cur_row = resolution * i 190 | if c < resolution - 1: 191 | triangles.extend([ 192 | prev_row + c + 1, prev_row + c, cur_row + c, 193 | prev_row + c + 1, cur_row + c, cur_row + c + 1 194 | ]) 195 | else: 196 | triangles.extend([ 197 | prev_row, prev_row + c, cur_row, 198 | cur_row, prev_row + c, cur_row + c 199 | ]) 200 | 201 | return (triangles, vertices, normals) 202 | --------------------------------------------------------------------------------