├── .gitignore ├── LICENSE ├── README.md ├── Run.bat ├── pyspace ├── camera.py ├── coloring.py ├── fold.py ├── frag.glsl ├── geo.py ├── object.py ├── shader.py ├── util.py └── vert.glsl ├── ray_marcher_demo.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.bmp 2 | *.mp4 3 | *.npy 4 | *.png 5 | *.prproj 6 | *.pyc 7 | 8 | frag_gen.glsl 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CodeParade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PySpace 2 | GLSL Fractal Ray Marcher in Python 3 | 4 | ## Installation 5 | 6 | ```bash 7 | pip install -r requirements.txt 8 | ``` 9 | 10 | ## Videos 11 | Overview: https://youtu.be/svLzmFuSBhk 12 | 13 | Examples: https://youtu.be/N8WWodGk9-g 14 | 15 | ODS Demo: https://youtu.be/yJyp7zEGKaU 16 | -------------------------------------------------------------------------------- /Run.bat: -------------------------------------------------------------------------------- 1 | python ray_marcher_demo.py 2 | pause 3 | -------------------------------------------------------------------------------- /pyspace/camera.py: -------------------------------------------------------------------------------- 1 | 2 | class Camera: 3 | def __init__(self): 4 | self.params = {} 5 | 6 | # Number of additional samples on each axis to average and improve image quality. 7 | # NOTE: This will slow down rendering quadratically. 8 | # Recommended Range: 1 to 8 (integer) 9 | self.params['ANTIALIASING_SAMPLES'] = 1 10 | 11 | # The strength of the ambient occlusion. 12 | # Recommended Range: 0.0 to 0.05 13 | self.params['AMBIENT_OCCLUSION_STRENGTH'] = 0.01 14 | 15 | # Represents the maximum RGB color shift that can result from ambient occlusion. 16 | # Typically use positive values for 'glow' effects and negative for 'darkness'. 17 | # Recommended Range: All values between -1.0 and 1.0 18 | self.params['AMBIENT_OCCLUSION_COLOR_DELTA'] = (0.8, 0.8, 0.8) 19 | 20 | # Color of the background when the ray doesn't hit anything 21 | # Recommended Range: All values between 0.0 and 1.0 22 | self.params['BACKGROUND_COLOR'] = (0.6, 0.6, 0.9) 23 | 24 | # The strength of the depth of field effect. 25 | # NOTE: ANTIALIASING_SAMPLES must be larger than 1 to use depth of field effects. 26 | # Recommended Range: 0.0 to 20.0 27 | self.params['DEPTH_OF_FIELD_STRENGTH'] = 0.0 28 | 29 | # The distance from the camera where objects will appear in focus. 30 | # NOTE: ANTIALIASING_SAMPLES must be larger than 1 to use depth of field effects. 31 | # Recommended Range: 0.0 to 100.0 32 | self.params['DEPTH_OF_FIELD_DISTANCE'] = 1.0 33 | 34 | # Determines if diffuse lighting should be enabled. 35 | # NOTE: This flag will be ignored if DIFFUSE_ENHANCED_ENABLED is set. 36 | # NOTE: SHADOW_DARKNESS is also used to change diffuse intensity. 37 | self.params['DIFFUSE_ENABLED'] = False 38 | 39 | # This is a 'less correct' but more aesthetically pleasing diffuse variant. 40 | # NOTE: This will override the DIFFUSE_ENABLED flag. 41 | # NOTE: SHADOW_DARKNESS is also used to change diffuse intensity. 42 | self.params['DIFFUSE_ENHANCED_ENABLED'] = True 43 | 44 | # Amount of light captured by the camera. 45 | # Can be used to increase/decrease brightness in case pixels are over-saturated. 46 | # Recommended Range: 0.1 to 10.0 47 | self.params['EXPOSURE'] = 1.0 48 | 49 | # Field of view of the camera in degrees 50 | # NOTE: This will have no effect if ORTHOGONAL_PROJECTION or ODS is enabled. 51 | # Recommended Range: 20.0 to 120.0 52 | self.params['FIELD_OF_VIEW'] = 60.0 53 | 54 | # When enabled, adds a distance-based fog to the scene 55 | # NOTE: Fog strength is determined by MAX_DIST and fog color is always BACKGROUND_COLOR. 56 | # NOTE: If enabled, you usually also want VIGNETTE_FOREGROUND=True and SUN_ENABLED=False. 57 | self.params['FOG_ENABLED'] = False 58 | 59 | # When enabled, adds object glow to the background layer. 60 | self.params['GLOW_ENABLED'] = False 61 | 62 | # Represents the maximum RGB color shift that can result from glow. 63 | # Recommended Range: All values between -1.0 and 1.0 64 | self.params['GLOW_COLOR_DELTA'] = (-0.2, 0.5, -0.2) 65 | 66 | # The sharpness of the glow. 67 | # Recommended Range: 1.0 to 100.0 68 | self.params['GLOW_SHARPNESS'] = 4.0 69 | 70 | # Color of the sunlight in RGB format. 71 | # Recommended Range: All values between 0.0 and 1.0 72 | self.params['LIGHT_COLOR'] = (1.0, 0.9, 0.6) 73 | 74 | # Direction of the sunlight. 75 | # NOTE: This must be a normalized quantity (magnitude = 1.0) 76 | self.params['LIGHT_DIRECTION'] = (-0.36, 0.48, 0.80) 77 | 78 | # Level of detail multiplier to improve speed and performance. 79 | # When this value is large, distant objects become less detailed. 80 | # Recommended Range: 0.0 to 100.0 81 | self.params['LOD_MULTIPLIER'] = 10.0 82 | 83 | # Maximum number of marches before a ray is considered a non-intersection. 84 | # Recommended Range: 10 to 10000 (integer) 85 | self.params['MAX_MARCHES'] = 1000 86 | 87 | # Maximum distance before a ray is considered a non-intersection. 88 | # Recommended Range: 0.5 to 200.0 89 | self.params['MAX_DIST'] = 50.0 90 | 91 | # Minimum march distance until a ray is considered intersecting. 92 | # Recommended Range: 0.000001 to 0.01 93 | self.params['MIN_DIST'] = 0.00001 94 | 95 | # Number of additional renderings between frames. 96 | # NOTE: This will slow down rendering linearly. 97 | # Recommended Range: 0 to 10 (integer) 98 | self.params['MOTION_BLUR_LEVEL'] = 0 99 | 100 | # Proportion of time the shutter is open during a frame 101 | # NOTE: Only applys when MOTION_BLUR_LEVEL is greater than 0 102 | # Recommended Range: 0.0 to 1.0 103 | self.params['MOTION_BLUR_RATIO'] = 1.0 104 | 105 | # If true will render an omnidirectional stereo 360 projection. 106 | self.params['ODS'] = False 107 | 108 | # Determines if the projection view should be orthographic instead of perspective. 109 | self.params['ORTHOGONAL_PROJECTION'] = False 110 | 111 | # When ORTHOGONAL_PROJECTION is enabled, this will determine the size of the view. 112 | # NOTE: Larger numbers mean the view will appear more zoomed out. 113 | # Recommended Range: 0.5 to 50.0 114 | self.params['ORTHOGONAL_ZOOM'] = 5.0 115 | 116 | # Number of additional bounces after a ray collides with the geometry. 117 | # Recommended Range: 0 to 8 (integer) 118 | self.params['REFLECTION_LEVEL'] = 0 119 | 120 | # Proportion of light lost each time a reflection occurs. 121 | # NOTE: This will only be relevant if REFLECTION_LEVEL is greater than 0. 122 | # Recommended Range: 0.25 to 1.0 123 | self.params['REFLECTION_ATTENUATION'] = 0.6 124 | 125 | # When enabled, uses an additional ray march to the light source to check for shadows. 126 | self.params['SHADOWS_ENABLED'] = True 127 | 128 | # Proportion of the light that is 'blocked' by the shadow or diffuse light. 129 | # Recommended Range: 0.0 to 1.0 130 | self.params['SHADOW_DARKNESS'] = 0.8 131 | 132 | # How sharp the shadows should appear. 133 | # Recommended Range: 1.0 to 100.0 134 | self.params['SHADOW_SHARPNESS'] = 16.0 135 | 136 | # Used to determine how sharp specular reflections are. 137 | # NOTE: This is the 'shininess' parameter in the phong illumination model. 138 | # NOTE: To disable specular highlights, use the value 0. 139 | # Recommended Range: 0 to 1000 (integer) 140 | self.params['SPECULAR_HIGHLIGHT'] = 40 141 | 142 | # Determines if the sun should be drawn in the sky. 143 | self.params['SUN_ENABLED'] = True 144 | 145 | # Size of the sun to draw in the sky. 146 | # NOTE: This only takes effect when SUN_ENABLED is enabled. 147 | # Recommended Range: 0.0001 to 0.05 148 | self.params['SUN_SIZE'] = 0.005 149 | 150 | # How sharp the sun should appear in the sky. 151 | # NOTE: This only takes effect when SUN_ENABLED is enabled. 152 | # Recommended Range: 0.1 to 10.0 153 | self.params['SUN_SHARPNESS'] = 2.0 154 | 155 | # When enabled, vignetting will also effect foreground objects. 156 | self.params['VIGNETTE_FOREGROUND'] = False 157 | 158 | # The strength of the vignetting effect. 159 | # Recommended Range: 0.0 to 1.5 160 | self.params['VIGNETTE_STRENGTH'] = 0.5 161 | 162 | def __getitem__(self, k): 163 | return self.params[k] 164 | 165 | def __setitem__(self, k, x): 166 | self.params[k] = x 167 | -------------------------------------------------------------------------------- /pyspace/coloring.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .util import * 3 | 4 | class OrbitInitZero: 5 | def __init__(self): 6 | pass 7 | 8 | def orbit(self): 9 | return '\tvec3 orbit = vec3(0.0);\n' 10 | 11 | class OrbitInitInf: 12 | def __init__(self): 13 | pass 14 | 15 | def orbit(self): 16 | return '\tvec3 orbit = vec3(1e20);\n' 17 | 18 | class OrbitInitNegInf: 19 | def __init__(self): 20 | pass 21 | 22 | def orbit(self): 23 | return '\tvec3 orbit = vec3(-1e20);\n' 24 | 25 | class OrbitMin: 26 | def __init__(self, scale=(1,1,1), origin=(0,0,0)): 27 | self.scale = set_global_vec3(scale) 28 | self.origin = set_global_vec3(origin) 29 | 30 | def orbit(self): 31 | if vec3_eq(self.origin, (0,0,0)): 32 | return '\torbit = min(orbit, p.xyz*' + vec3_str(self.scale) + ');\n' 33 | else: 34 | return '\torbit = min(orbit, (p.xyz - ' + vec3_str(self.origin) + ')*' + vec3_str(self.scale) + ');\n' 35 | 36 | class OrbitMinAbs: 37 | def __init__(self, scale=(1,1,1), origin=(0,0,0)): 38 | self.scale = set_global_vec3(scale) 39 | self.origin = set_global_vec3(origin) 40 | 41 | def orbit(self): 42 | return '\torbit = min(orbit, abs((p.xyz - ' + vec3_str(self.origin) + ')*' + vec3_str(self.scale) + '));\n' 43 | 44 | class OrbitMax: 45 | def __init__(self, scale=(1,1,1), origin=(0,0,0)): 46 | self.scale = set_global_vec3(scale) 47 | self.origin = set_global_vec3(origin) 48 | 49 | def orbit(self): 50 | return '\torbit = max(orbit, (p.xyz - ' + vec3_str(self.origin) + ')*' + vec3_str(self.scale) + ');\n' 51 | 52 | class OrbitMaxAbs: 53 | def __init__(self, scale=(1,1,1), origin=(0,0,0)): 54 | self.scale = set_global_vec3(scale) 55 | self.origin = set_global_vec3(origin) 56 | 57 | def orbit(self): 58 | return '\torbit = max(orbit, abs((p.xyz - ' + vec3_str(self.origin) + ')*' + vec3_str(self.scale) + '));\n' 59 | 60 | class OrbitSum: 61 | def __init__(self, scale=(1,1,1), origin=(0,0,0)): 62 | self.scale = set_global_vec3(scale) 63 | self.origin = set_global_vec3(origin) 64 | 65 | def orbit(self): 66 | return '\torbit += (p.xyz - ' + vec3_str(self.origin) + ')*' + vec3_str(self.scale) + ';\n' 67 | 68 | class OrbitSumAbs: 69 | def __init__(self, scale=(1,1,1), origin=(0,0,0)): 70 | self.scale = set_global_vec3(scale) 71 | self.origin = set_global_vec3(origin) 72 | 73 | def orbit(self): 74 | return '\torbit += abs((p.xyz - ' + vec3_str(self.origin) + ')*' + vec3_str(self.scale) + ');\n' 75 | -------------------------------------------------------------------------------- /pyspace/fold.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from .util import * 4 | 5 | class FoldPlane: 6 | def __init__(self, n, d=0.0): 7 | self.n = set_global_vec3(n) 8 | self.d = set_global_float(d) 9 | 10 | def fold(self, p): 11 | n = get_global(self.n) 12 | d = get_global(self.d) 13 | p[:3] -= 2.0 * min(0.0, np.dot(p[:3], n) - d) * n 14 | 15 | def unfold(self, p, q): 16 | n = get_global(self.n) 17 | d = get_global(self.d) 18 | if np.dot(p[:3], n) - d < 0.0: 19 | q[:3] -= 2.0 * (np.dot(q[:3], n) - d) * n 20 | 21 | def glsl(self): 22 | if vec3_eq(self.n, (1,0,0)): 23 | return '\tp.x = abs(p.x - ' + float_str(self.d) + ') + ' + float_str(self.d) + ';\n' 24 | elif vec3_eq(self.n, (0,1,0)): 25 | return '\tp.y = abs(p.y - ' + float_str(self.d) + ') + ' + float_str(self.d) + ';\n' 26 | elif vec3_eq(self.n, (0,0,1)): 27 | return '\tp.z = abs(p.z - ' + float_str(self.d) + ') + ' + float_str(self.d) + ';\n' 28 | elif vec3_eq(self.n, (-1,0,0)): 29 | return '\tp.x = -abs(p.x + ' + float_str(self.d) + ') - ' + float_str(self.d) + ';\n' 30 | elif vec3_eq(self.n, (0,-1,0)): 31 | return '\tp.y = -abs(p.y + ' + float_str(self.d) + ') - ' + float_str(self.d) + ';\n' 32 | elif vec3_eq(self.n, (0,0,-1)): 33 | return '\tp.z = -abs(p.z + ' + float_str(self.d) + ') - ' + float_str(self.d) + ';\n' 34 | else: 35 | return '\tplaneFold(p, ' + vec3_str(self.n) + ', ' + float_str(self.d) + ');\n' 36 | 37 | ''' 38 | EQUIVALENT FOLD: 39 | FoldPlane((1, 0, 0), c_x)) 40 | FoldPlane((0, 1, 0), c_y)) 41 | FoldPlane((0, 0, 1), c_z)) 42 | ''' 43 | class FoldAbs: 44 | def __init__(self, c=(0,0,0)): 45 | self.c = set_global_vec3(c) 46 | 47 | def fold(self, p): 48 | c = get_global(self.c) 49 | p[:3] = np.abs(p[:3] - c) + c 50 | 51 | def unfold(self, p, q): 52 | c = get_global(self.c) 53 | if p[0] < c[0]: q[0] = 2*c[0] - q[0] 54 | if p[1] < c[1]: q[1] = 2*c[0] - q[1] 55 | if p[2] < c[2]: q[2] = 2*c[0] - q[2] 56 | 57 | def glsl(self): 58 | if vec3_eq(self.c, (0,0,0)): 59 | return '\tp.xyz = abs(p.xyz);\n' 60 | else: 61 | return '\tabsFold(p, ' + vec3_str(self.c) + ');\n' 62 | 63 | ''' 64 | EQUIVALENT FOLD: 65 | FoldPlane((inv_sqrt2, inv_sqrt2, 0))) 66 | FoldPlane((inv_sqrt2, 0, inv_sqrt2))) 67 | FoldPlane((0, inv_sqrt2, inv_sqrt2))) 68 | ''' 69 | class FoldSierpinski: 70 | def __init__(self): 71 | pass 72 | 73 | def fold(self, p): 74 | if p[0] + p[1] < 0.0: 75 | p[[0,1]] = -p[[1,0]] 76 | if p[0] + p[2] < 0.0: 77 | p[[0,2]] = -p[[2,0]] 78 | if p[1] + p[2] < 0.0: 79 | p[[2,1]] = -p[[1,2]] 80 | 81 | def unfold(self, p, q): 82 | mx = max(-p[1], p[0]) 83 | if max(p[1], -p[0]) + max(-mx, p[2]) < 0.0: 84 | q[[2,1]] = -q[[1,2]] 85 | if mx + p[2] < 0.0: 86 | q[[0,2]] = -q[[2,0]] 87 | if p[0] + p[1] < 0.0: 88 | q[[0,1]] = -q[[1,0]] 89 | 90 | def glsl(self): 91 | return '\tsierpinskiFold(p);\n' 92 | 93 | ''' 94 | EQUIVALENT FOLD: 95 | FoldPlane((inv_sqrt2, -inv_sqrt2, 0))) 96 | FoldPlane((inv_sqrt2, 0, -inv_sqrt2))) 97 | FoldPlane((0, inv_sqrt2, -inv_sqrt2))) 98 | ''' 99 | class FoldMenger: 100 | def __init__(self): 101 | pass 102 | 103 | def fold(self, p): 104 | if p[0] < p[1]: 105 | p[[0,1]] = p[[1,0]] 106 | if p[0] < p[2]: 107 | p[[0,2]] = p[[2,0]] 108 | if p[1] < p[2]: 109 | p[[2,1]] = p[[1,2]] 110 | 111 | def unfold(self, p, q): 112 | mx = max(p[0], p[1]) 113 | if min(p[0], p[1]) < min(mx, p[2]): 114 | q[[2,1]] = q[[1,2]] 115 | if mx < p[2]: 116 | q[[0,2]] = q[[2,0]] 117 | if p[0] < p[1]: 118 | q[[0,1]] = q[[1,0]] 119 | 120 | def glsl(self): 121 | return '\tmengerFold(p);\n' 122 | 123 | class FoldScaleTranslate: 124 | def __init__(self, s=1.0, t=(0,0,0)): 125 | self.s = set_global_float(s) 126 | self.t = set_global_vec3(t) 127 | 128 | def fold(self, p): 129 | p[:] *= get_global(self.s) 130 | p[:3] += get_global(self.t) 131 | 132 | def unfold(self, p, q): 133 | q[:3] -= get_global(self.t) 134 | q[:] /= get_global(self.s) 135 | 136 | def glsl(self): 137 | ret_str = '' 138 | if self.s != 1.0: 139 | if isinstance(self.s, (float, int)) and self.s >= 0: 140 | ret_str += '\tp *= ' + float_str(self.s) + ';\n' 141 | else: 142 | ret_str += '\tp.xyz *= ' + float_str(self.s) + ';\n' 143 | ret_str += '\tp.w *= abs(' + float_str(self.s) + ');\n' 144 | if not np.array_equal(self.t, np.zeros((3,), dtype=np.float32)): 145 | ret_str += '\tp.xyz += ' + vec3_str(self.t) + ';\n' 146 | return ret_str 147 | 148 | class FoldScaleOrigin: 149 | def __init__(self, s=1.0): 150 | self.s = set_global_float(s) 151 | self.o = set_global_vec3((0,0,0)) 152 | 153 | def fold(self, p): 154 | p[:] = p*get_global(self.s) + self.o 155 | 156 | def unfold(self, p, q): 157 | q[:] = (q - self.o[:3]) / get_global(self.s) 158 | 159 | def glsl(self): 160 | ret_str = '' 161 | if self.s != 1.0: 162 | ret_str += '\tp = p*' + float_str(self.s) + ';p.w = abs(p.w);p += o;\n' 163 | else: 164 | ret_str += '\tp += o;\n' 165 | return ret_str 166 | 167 | class FoldBox: 168 | def __init__(self, r=(1.0,1.0,1.0)): 169 | self.r = set_global_vec3(r) 170 | 171 | def fold(self, p): 172 | r = get_global(self.r) 173 | p[:3] = np.clip(p[:3], -r, r)*2 - p[:3] 174 | 175 | def unfold(self, p, q): 176 | r = get_global(self.r) 177 | if p[0] < -r[0]: q[0] = -2*r[0] - q[0] 178 | if p[1] < -r[1]: q[1] = -2*r[1] - q[1] 179 | if p[2] < -r[2]: q[2] = -2*r[2] - q[2] 180 | if p[0] > r[0]: q[0] = 2*r[0] - q[0] 181 | if p[1] > r[1]: q[1] = 2*r[1] - q[1] 182 | if p[2] > r[2]: q[2] = 2*r[2] - q[2] 183 | 184 | def glsl(self): 185 | return '\tboxFold(p,' + vec3_str(self.r) + ');\n' 186 | 187 | class FoldSphere: 188 | def __init__(self, min_r=0.5, max_r=1.0): 189 | self.min_r = set_global_float(min_r) 190 | self.max_r = set_global_float(max_r) 191 | 192 | def fold(self, p): 193 | max_r = get_global(self.max_r) 194 | min_r = get_global(self.min_r) 195 | r2 = np.dot(p[:3], p[:3]) 196 | p[:] *= max(max_r / max(min_r, r2), 1.0) 197 | 198 | def unfold(self, p, q): 199 | max_r = get_global(self.max_r) 200 | min_r = get_global(self.min_r) 201 | r2 = np.dot(p[:3], p[:3]) 202 | q[:] /= max(max_r / max(min_r, r2), 1.0) 203 | 204 | def glsl(self): 205 | return '\tsphereFold(p,' + float_str(self.min_r) + ',' + float_str(self.max_r) + ');\n' 206 | 207 | class FoldInversion: 208 | def __init__(self, epsilon=1e-12): 209 | self.epsilon = set_global_float(epsilon) 210 | 211 | def fold(self, p): 212 | epsilon = get_global(self.epsilon) 213 | p[:] *= 1.0 / (np.dot(p[:3], p[:3]) + epsilon) 214 | 215 | def unfold(self, p, q): 216 | epsilon = get_global(self.epsilon) 217 | q[:] *= (np.dot(p[:3], p[:3]) + epsilon) 218 | 219 | def glsl(self): 220 | return '\tp *= 1.0 / (dot(p.xyz, p.xyz) + ' + float_str(self.epsilon) + ');\n' 221 | 222 | class FoldRotateX: 223 | def __init__(self, a): 224 | self.a = set_global_float(a) 225 | 226 | def fold(self, p): 227 | a = get_global(self.a) 228 | s,c = math.sin(a), math.cos(a) 229 | p[1], p[2] = (c*p[1] + s*p[2]), (c*p[2] - s*p[1]) 230 | 231 | def unfold(self, p, q): 232 | a = get_global(self.a) 233 | s,c = math.sin(-a), math.cos(-a) 234 | q[1], q[2] = (c*q[1] + s*q[2]), (c*q[2] - s*q[1]) 235 | 236 | def glsl(self): 237 | if isinstance(self.a, (float, int)): 238 | return '\trotX(p, ' + float_str(math.sin(self.a)) + ', ' + float_str(math.cos(self.a)) + ');\n' 239 | else: 240 | return '\trotX(p, ' + float_str(self.a) + ');\n' 241 | 242 | class FoldRotateY: 243 | def __init__(self, a): 244 | self.a = set_global_float(a) 245 | 246 | def fold(self, p): 247 | a = get_global(self.a) 248 | s,c = math.sin(a), math.cos(a) 249 | p[2], p[0] = (c*p[2] + s*p[0]), (c*p[0] - s*p[2]) 250 | 251 | def unfold(self, p, q): 252 | a = get_global(self.a) 253 | s,c = math.sin(-a), math.cos(-a) 254 | q[2], q[0] = (c*q[2] + s*q[0]), (c*q[0] - s*q[2]) 255 | 256 | def glsl(self): 257 | if isinstance(self.a, (float, int)): 258 | return '\trotY(p, ' + float_str(math.sin(self.a)) + ', ' + float_str(math.cos(self.a)) + ');\n' 259 | else: 260 | return '\trotY(p, ' + float_str(self.a) + ');\n' 261 | 262 | class FoldRotateZ: 263 | def __init__(self, a): 264 | self.a = set_global_float(a) 265 | 266 | def fold(self, p): 267 | a = get_global(self.a) 268 | s,c = math.sin(a), math.cos(a) 269 | p[0], p[1] = (c*p[0] + s*p[1]), (c*p[1] - s*p[0]) 270 | 271 | def unfold(self, p, q): 272 | a = get_global(self.a) 273 | s,c = math.sin(-a), math.cos(-a) 274 | q[0], q[1] = (c*q[0] + s*q[1]), (c*q[1] - s*q[0]) 275 | 276 | def glsl(self): 277 | if isinstance(self.a, (float, int)): 278 | return '\trotZ(p, ' + float_str(math.sin(self.a)) + ', ' + float_str(math.cos(self.a)) + ');\n' 279 | else: 280 | return '\trotZ(p, ' + float_str(self.a) + ');\n' 281 | 282 | class FoldRepeatX: 283 | def __init__(self, m): 284 | self.m = set_global_float(m) 285 | 286 | def fold(self, p): 287 | m = get_global(self.m) 288 | p[0] = abs((p[0] - m/2) % m - m/2) 289 | 290 | def unfold(self, p, q): 291 | m = get_global(self.m) 292 | a = (p[0] - m/2) % m - m/2 293 | if a < 0.0: q[0] = -q[0] 294 | q[0] += p[0] - a 295 | 296 | def glsl(self): 297 | return '\tp.x = abs(mod(p.x - ' + float_str(self.m) + '/2,' + float_str(self.m) + ') - ' + float_str(self.m) + '/2);\n' 298 | 299 | class FoldRepeatY: 300 | def __init__(self, m): 301 | self.m = set_global_float(m) 302 | 303 | def fold(self, p): 304 | m = get_global(self.m) 305 | p[1] = abs((p[1] - m/2) % m - m/2) 306 | 307 | def unfold(self, p, q): 308 | m = get_global(self.m) 309 | a = (p[1] - m/2) % m - m/2 310 | if a < 0.0: q[1] = -q[1] 311 | q[1] += p[1] - a 312 | 313 | def glsl(self): 314 | return '\tp.y = abs(mod(p.y - ' + float_str(self.m) + '/2,' + float_str(self.m) + ') - ' + float_str(self.m) + '/2);\n' 315 | 316 | class FoldRepeatZ: 317 | def __init__(self, m): 318 | self.m = set_global_float(m) 319 | 320 | def fold(self, p): 321 | m = get_global(self.m) 322 | p[2] = abs((p[2] - m/2) % m - m/2) 323 | 324 | def unfold(self, p, q): 325 | m = get_global(self.m) 326 | a = (p[2] - m/2) % m - m/2 327 | if a < 0.0: q[2] = -q[2] 328 | q[2] += p[2] - a 329 | 330 | def glsl(self): 331 | return '\tp.z = abs(mod(p.z - ' + float_str(self.m) + '/2,' + float_str(self.m) + ') - ' + float_str(self.m) + '/2);\n' 332 | 333 | class FoldRepeatXYZ: 334 | def __init__(self, m): 335 | self.m = set_global_float(m) 336 | 337 | def fold(self, p): 338 | m = get_global(self.m) 339 | p[:3] = abs((p[:3] - m/2) % m - m/2) 340 | 341 | def unfold(self, p, q): 342 | m = get_global(self.m) 343 | a = (p[:3] - m/2) % m - m/2 344 | if a[0] < 0.0: q[0] = -q[0] 345 | if a[1] < 0.0: q[1] = -q[1] 346 | if a[2] < 0.0: q[2] = -q[2] 347 | q[:3] += p[:3] - a 348 | 349 | def glsl(self): 350 | return '\tp.xyz = abs(mod(p.xyz - ' + float_str(self.m) + '/2,' + float_str(self.m) + ') - ' + float_str(self.m) + '/2);\n' 351 | -------------------------------------------------------------------------------- /pyspace/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 120 2 | #define M_PI 3.14159265358979 3 | 4 | uniform mat4 iMat; 5 | uniform mat4 iPrevMat; 6 | uniform vec2 iResolution; 7 | uniform float iIPD; 8 | 9 | // [pydefine] 10 | // [/pydefine] 11 | 12 | // [pyvars] 13 | // [/pyvars] 14 | 15 | const float FOCAL_DIST = 1.0 / tan(M_PI * FIELD_OF_VIEW / 360.0); 16 | 17 | float rand(float s, float minV, float maxV) { 18 | float r = sin(s*s*27.12345 + 1000.9876 / (s*s + 1e-5)); 19 | return (r + 1.0) * 0.5 * (maxV - minV) + minV; 20 | } 21 | float smin(float a, float b, float k) { 22 | float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0 ); 23 | return mix(b, a, h) - k*h*(1.0 - h); 24 | //return -log(exp(-a/k) + exp(-b/k))*k; 25 | } 26 | 27 | //########################################## 28 | // 29 | // Space folding functions 30 | // 31 | //########################################## 32 | void planeFold(inout vec4 z, vec3 n, float d) { 33 | z.xyz -= 2.0 * min(0.0, dot(z.xyz, n) - d) * n; 34 | } 35 | void absFold(inout vec4 z, vec3 c) { 36 | z.xyz = abs(z.xyz - c) + c; 37 | } 38 | void sierpinskiFold(inout vec4 z) { 39 | z.xy -= min(z.x + z.y, 0.0); 40 | z.xz -= min(z.x + z.z, 0.0); 41 | z.yz -= min(z.y + z.z, 0.0); 42 | } 43 | void mengerFold(inout vec4 z) { 44 | float a = min(z.x - z.y, 0.0); 45 | z.x -= a; 46 | z.y += a; 47 | a = min(z.x - z.z, 0.0); 48 | z.x -= a; 49 | z.z += a; 50 | a = min(z.y - z.z, 0.0); 51 | z.y -= a; 52 | z.z += a; 53 | } 54 | void sphereFold(inout vec4 z, float minR, float maxR) { 55 | float r2 = dot(z.xyz, z.xyz); 56 | z *= max(maxR / max(minR, r2), 1.0); 57 | } 58 | void boxFold(inout vec4 z, vec3 r) { 59 | z.xyz = clamp(z.xyz, -r, r) * 2.0 - z.xyz; 60 | } 61 | void rotX(inout vec4 z, float s, float c) { 62 | z.yz = vec2(c*z.y + s*z.z, c*z.z - s*z.y); 63 | } 64 | void rotY(inout vec4 z, float s, float c) { 65 | z.xz = vec2(c*z.x - s*z.z, c*z.z + s*z.x); 66 | } 67 | void rotZ(inout vec4 z, float s, float c) { 68 | z.xy = vec2(c*z.x + s*z.y, c*z.y - s*z.x); 69 | } 70 | void rotX(inout vec4 z, float a) { 71 | rotX(z, sin(a), cos(a)); 72 | } 73 | void rotY(inout vec4 z, float a) { 74 | rotY(z, sin(a), cos(a)); 75 | } 76 | void rotZ(inout vec4 z, float a) { 77 | rotZ(z, sin(a), cos(a)); 78 | } 79 | 80 | //########################################## 81 | // 82 | // Primative distance estimators 83 | // 84 | //########################################## 85 | float de_sphere(vec4 p, float r) { 86 | return (length(p.xyz) - r) / p.w; 87 | } 88 | float de_box(vec4 p, vec3 s) { 89 | vec3 a = abs(p.xyz) - s; 90 | return (min(max(max(a.x, a.y), a.z), 0.0) + length(max(a, 0.0))) / p.w; 91 | } 92 | float de_tetrahedron(vec4 p, float r) { 93 | float md = max(max(-p.x - p.y - p.z, p.x + p.y - p.z), 94 | max(-p.x + p.y + p.z, p.x - p.y + p.z)); 95 | return (md - r) / (p.w * sqrt(3.0)); 96 | } 97 | float de_inf_cross(vec4 p, float r) { 98 | vec3 q = p.xyz * p.xyz; 99 | return (sqrt(min(min(q.x + q.y, q.x + q.z), q.y + q.z)) - r) / p.w; 100 | } 101 | float de_inf_cross_xy(vec4 p, float r) { 102 | vec3 q = p.xyz * p.xyz; 103 | return (sqrt(min(q.x, q.y) + q.z) - r) / p.w; 104 | } 105 | float de_inf_line(vec4 p, vec3 n, float r) { 106 | return (length(p.xyz - n*dot(p.xyz, n)) - r) / p.w; 107 | } 108 | 109 | //########################################## 110 | // 111 | // Compiled 112 | // 113 | //########################################## 114 | 115 | // [pyspace] 116 | // [/pyspace] 117 | 118 | //########################################## 119 | // 120 | // Main code 121 | // 122 | //########################################## 123 | vec4 ray_march(inout vec4 p, vec4 ray, float sharpness, float td) { 124 | //March the ray 125 | float d = MIN_DIST; 126 | float s = 0.0; 127 | float min_d = 1.0; 128 | for (; s < MAX_MARCHES; s += 1.0) { 129 | d = DE(p); 130 | if (d < MIN_DIST) { 131 | s += d / MIN_DIST; 132 | break; 133 | } else if (td > MAX_DIST) { 134 | break; 135 | } 136 | td += d; 137 | p += ray * d; 138 | min_d = min(min_d, sharpness * d / td); 139 | } 140 | return vec4(d, s, td, min_d); 141 | } 142 | vec4 ray_march(inout vec4 p, vec3 ray, float sharpness, float td) { 143 | return ray_march(p, vec4(ray, 0.0), sharpness, td); 144 | } 145 | 146 | //A faster formula to find the gradient/normal direction of the DE(the w component is the average DE) 147 | //credit to http://www.iquilezles.org/www/articles/normalsSDF/normalsSDF.htm 148 | vec3 calcNormal(vec4 p, float dx) { 149 | const vec3 k = vec3(1,-1,0); 150 | return normalize(k.xyy*DE(p + k.xyyz*dx) + 151 | k.yyx*DE(p + k.yyxz*dx) + 152 | k.yxy*DE(p + k.yxyz*dx) + 153 | k.xxx*DE(p + k.xxxz*dx)); 154 | } 155 | 156 | vec4 scene(inout vec4 origin, inout vec4 ray, float vignette, float td) { 157 | //Trace the ray 158 | vec4 p = origin; 159 | vec4 d_s_td_m = ray_march(p, ray, GLOW_SHARPNESS, td); 160 | float d = d_s_td_m.x; 161 | float s = d_s_td_m.y; 162 | td = d_s_td_m.z; 163 | float m = d_s_td_m.w; 164 | 165 | //Determine the color for this pixel 166 | vec3 col = vec3(0.0); 167 | float min_dist = MIN_DIST * max(td * LOD_MULTIPLIER, 1.0); 168 | if (d < min_dist) { 169 | //Get the surface normal 170 | vec3 n = calcNormal(p, MIN_DIST * 10); 171 | vec3 reflected = ray.xyz - 2.0*dot(ray.xyz, n) * n; 172 | 173 | //Get coloring 174 | vec3 orig_col = clamp(COL(p).xyz, 0.0, 1.0); 175 | 176 | //Get if this point is in shadow 177 | float k = 1.0; 178 | #if SHADOWS_ENABLED 179 | vec4 light_pt = p; 180 | light_pt.xyz += n * min_dist * 10; 181 | vec4 rm = ray_march(light_pt, LIGHT_DIRECTION, SHADOW_SHARPNESS, 0.0); 182 | k = rm.w * min(rm.z, 1.0); 183 | #endif 184 | 185 | //Get specular 186 | #if SPECULAR_HIGHLIGHT > 0 187 | float specular = max(dot(reflected, LIGHT_DIRECTION), 0.0); 188 | specular = pow(specular, SPECULAR_HIGHLIGHT); 189 | col += specular * LIGHT_COLOR * k; 190 | #endif 191 | 192 | //Get diffuse lighting 193 | #if DIFFUSE_ENHANCED_ENABLED 194 | k = min(k, SHADOW_DARKNESS * 0.5 * (dot(n, LIGHT_DIRECTION) - 1.0) + 1.0); 195 | #elif DIFFUSE_ENABLED 196 | k = min(k, dot(n, LIGHT_DIRECTION)); 197 | #endif 198 | 199 | //Don't make shadows entirely dark 200 | k = max(k, 1.0 - SHADOW_DARKNESS); 201 | col += orig_col * LIGHT_COLOR * k; 202 | 203 | //Add small amount of ambient occlusion 204 | float a = 1.0 / (1.0 + s * AMBIENT_OCCLUSION_STRENGTH); 205 | col += (1.0 - a) * AMBIENT_OCCLUSION_COLOR_DELTA; 206 | 207 | //Add fog effects 208 | #if FOG_ENABLED 209 | a = td / MAX_DIST; 210 | col = (1.0 - a) * col + a * BACKGROUND_COLOR; 211 | #endif 212 | 213 | //Set up the reflection 214 | origin = p + vec4(n * min_dist * 100, 0.0); 215 | ray = vec4(reflected, 0.0); 216 | 217 | //Apply vignette if needed 218 | #if VIGNETTE_FOREGROUND 219 | col *= vignette; 220 | #endif 221 | } else { 222 | //Ray missed, start with solid background color 223 | col += BACKGROUND_COLOR; 224 | 225 | //Apply glow 226 | #if GLOW_ENABLED 227 | col += (1.0 - m) * (1.0 - m) * GLOW_COLOR_DELTA; 228 | #endif 229 | 230 | col *= vignette; 231 | //Background specular 232 | #if SUN_ENABLED 233 | float sun_spec = dot(ray.xyz, LIGHT_DIRECTION) - 1.0 + SUN_SIZE; 234 | sun_spec = min(exp(sun_spec * SUN_SHARPNESS / SUN_SIZE), 1.0); 235 | col += LIGHT_COLOR * sun_spec; 236 | #endif 237 | } 238 | return vec4(col, td); 239 | } 240 | 241 | void main() { 242 | vec4 col = vec4(0.0); 243 | for (int k = 0; k < MOTION_BLUR_LEVEL + 1; ++k) { 244 | for (int i = 0; i < ANTIALIASING_SAMPLES; ++i) { 245 | for (int j = 0; j < ANTIALIASING_SAMPLES; ++j) { 246 | mat4 mat = iMat; 247 | #if MOTION_BLUR_LEVEL > 0 248 | float a = MOTION_BLUR_RATIO * float(k) / (MOTION_BLUR_LEVEL + 1); 249 | mat = iPrevMat*a + iMat*(1.0 - a); 250 | #endif 251 | 252 | vec2 delta = vec2(i, j) / ANTIALIASING_SAMPLES; 253 | vec2 delta2 = vec2(rand(i,0,1), rand(j+0.1,0,1)); 254 | vec4 dxy = vec4(delta2.x, delta2.y, 0.0, 0.0) * DEPTH_OF_FIELD_STRENGTH / iResolution.x; 255 | 256 | #if ODS 257 | //Get the normalized screen coordinate 258 | vec2 screen_pos; 259 | float scale; 260 | vec2 ods_coord = vec2(gl_FragCoord.x, gl_FragCoord.y * 2); 261 | if (ods_coord.y < iResolution.y) { 262 | screen_pos = (ods_coord + delta) / iResolution.xy; 263 | scale = -iIPD*0.5; 264 | } else { 265 | screen_pos = (ods_coord - vec2(0, iResolution.y) + delta) / iResolution.xy; 266 | scale = iIPD*0.5; 267 | } 268 | float theta = -2*M_PI*screen_pos.x; 269 | float phi = -0.5*M_PI + screen_pos.y*M_PI; 270 | scale *= cos(phi); 271 | vec4 p = mat[3] - mat * vec4(cos(theta) * scale, 0, sin(theta) * scale, 0); 272 | vec4 ray = mat * vec4(sin(theta)*cos(phi), sin(phi), cos(theta)*cos(phi), 0); 273 | #else 274 | //Get normalized screen coordinate 275 | vec2 screen_pos = (gl_FragCoord.xy + delta) / iResolution.xy; 276 | vec2 uv = 2*screen_pos - 1; 277 | uv.x *= iResolution.x / iResolution.y; 278 | 279 | //Convert screen coordinate to 3d ray 280 | #if ORTHOGONAL_PROJECTION 281 | vec4 ray = vec4(0.0, 0.0, -FOCAL_DIST, 0.0); 282 | ray = mat * normalize(ray); 283 | #else 284 | vec4 ray = normalize(vec4(uv.x, uv.y, -FOCAL_DIST, 0.0)); 285 | ray = mat * normalize(ray * DEPTH_OF_FIELD_DISTANCE + dxy); 286 | #endif 287 | 288 | //Cast the first ray 289 | #if ORTHOGONAL_PROJECTION 290 | vec4 p = mat[3] + mat * vec4(uv.x, uv.y, 0.0, 0.0) * ORTHOGONAL_ZOOM; 291 | #else 292 | vec4 p = mat[3] - mat * dxy; 293 | #endif 294 | #endif 295 | 296 | //Reflect light if needed 297 | float vignette = 1.0 - VIGNETTE_STRENGTH * length(screen_pos - 0.5); 298 | #if REFLECTION_LEVEL > 0 299 | vec3 newCol = vec3(0.0); 300 | vec4 prevRay = ray; 301 | float ref_alpha = 1.0; 302 | for (int r = 0; r < REFLECTION_LEVEL + 1; ++r) { 303 | ref_alpha *= REFLECTION_ATTENUATION; 304 | newCol += ref_alpha * scene(p, ray, vignette, 0.0); 305 | if (ray == prevRay || r >= REFLECTION_LEVEL) { 306 | break; 307 | } 308 | } 309 | col += newCol; 310 | #else 311 | col += scene(p, ray, vignette, 0.0); 312 | #endif 313 | 314 | } 315 | } 316 | } 317 | 318 | col /= (ANTIALIASING_SAMPLES * ANTIALIASING_SAMPLES * (MOTION_BLUR_LEVEL + 1)); 319 | gl_FragColor.rgb = clamp(col.xyz * EXPOSURE, 0.0, 1.0); 320 | gl_FragDepth = min(col.w / MAX_DIST, 0.999); 321 | } 322 | -------------------------------------------------------------------------------- /pyspace/geo.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from .util import * 4 | 5 | class Sphere: 6 | def __init__(self, r=1.0, c=(0,0,0), color=(1,1,1)): 7 | self.r = set_global_float(r) 8 | self.c = set_global_vec3(c) 9 | self.color = color 10 | 11 | def DE(self, p): 12 | c = get_global(self.c) 13 | r = get_global(self.r) 14 | return (np.linalg.norm(p[:3] - c) - r) / p[3] 15 | 16 | def NP(self, p): 17 | c = get_global(self.c) 18 | r = get_global(self.r) 19 | return normalize(p[:3] - c) * r + c 20 | 21 | def glsl(self): 22 | return 'de_sphere(p' + cond_offset(self.c) + ', ' + float_str(self.r) + ')' 23 | 24 | def glsl_col(self): 25 | return make_color(self) 26 | 27 | class Box: 28 | def __init__(self, s=(1,1,1), c=(0,0,0), color=(1,1,1)): 29 | self.s = set_global_vec3(s) 30 | self.c = set_global_vec3(c) 31 | self.color = color 32 | 33 | def DE(self, p): 34 | c = get_global(self.c) 35 | s = get_global(self.s) 36 | a = np.abs(p[:3] - c) - s; 37 | return (min(max(a[0], a[1], a[2]), 0.0) + np.linalg.norm(np.maximum(a,0.0))) / p[3] 38 | 39 | def NP(self, p): 40 | c = get_global(self.c) 41 | s = get_global(self.s) 42 | return np.clip(p[:3] - c, -s, s) + c 43 | 44 | def glsl(self): 45 | return 'de_box(p' + cond_offset(self.c) + ', ' + vec3_str(self.s) + ')' 46 | 47 | def glsl_col(self): 48 | return make_color(self) 49 | 50 | class Tetrahedron: 51 | def __init__(self, r=1.0, c=(0,0,0), color=(1,1,1)): 52 | self.r = set_global_float(r) 53 | self.c = set_global_vec3(c) 54 | self.color = color 55 | 56 | def DE(self, p): 57 | c = get_global(self.c) 58 | r = get_global(self.r) 59 | a = p[:3] - c 60 | md = max(-a[0] - a[1] - a[2], a[0] + a[1] - a[2], -a[0] + a[1] + a[2], a[0] - a[1] + a[2]) 61 | return (md - r) / (p[3] * math.sqrt(3.0)); 62 | 63 | def NP(self, p): 64 | raise Exception("Not implemented") 65 | 66 | def glsl(self): 67 | return 'de_tetrahedron(p' + cond_offset(self.c) + ', ' + float_str(self.r) + ')' 68 | 69 | def glsl_col(self): 70 | return make_color(self) 71 | 72 | class InfCross: 73 | def __init__(self, r=1.0, c=(0,0,0), color=(1,1,1)): 74 | self.r = set_global_float(r) 75 | self.c = set_global_vec3(c) 76 | self.color = color 77 | 78 | def DE(self, p): 79 | r = get_global(self.r) 80 | c = get_global(self.c) 81 | sq = (p[:3] - c) * (p[:3] - c) 82 | return (math.sqrt(min(min(sq[0] + sq[1], sq[0] + sq[2]), sq[1] + sq[2])) - r) / p[3] 83 | 84 | def NP(self, p): 85 | r = get_global(self.r) 86 | c = get_global(self.c) 87 | n = p[:3] - c 88 | m_ix = np.argmax(np.abs(n)) 89 | n[m_ix] = 0.0 90 | n = normalize(n) * r 91 | n[m_ix] = p[m_ix] - c[m_ix] 92 | return n + c 93 | 94 | def glsl(self): 95 | return 'de_inf_cross(p' + cond_offset(self.c) + ', ' + float_str(self.r) + ')' 96 | 97 | def glsl_col(self): 98 | return make_color(self) 99 | 100 | class InfCrossXY: 101 | def __init__(self, r=1.0, c=(0,0,0), color=(1,1,1)): 102 | self.r = set_global_float(r) 103 | self.c = set_global_vec3(c) 104 | self.color = color 105 | 106 | def DE(self, p): 107 | r = get_global(self.r) 108 | c = get_global(self.c) 109 | sq = (p[:3] - c) * (p[:3] - c) 110 | return (math.sqrt(min(sq[0], sq[1]) + sq[2]) - r) / p[3] 111 | 112 | def NP(self, p): 113 | r = get_global(self.r) 114 | c = get_global(self.c) 115 | n = p[:3] - c 116 | m_ix = (0 if abs(n[0]) > abs(n[1]) else 1) 117 | n[m_ix] = 0.0 118 | n = normalize(n) * r 119 | n[m_ix] = p[m_ix] - c[m_ix] 120 | return n + c 121 | 122 | def glsl(self): 123 | return 'de_inf_cross_xy(p' + cond_offset(self.c) + ', ' + float_str(self.r) + ')' 124 | 125 | def glsl_col(self): 126 | return make_color(self) 127 | 128 | class InfLine: 129 | def __init__(self, r=1.0, n=(1,0,0), c=(0,0,0), color=(1,1,1)): 130 | self.r = set_global_float(r) 131 | self.n = set_global_vec3(n) 132 | self.c = set_global_vec3(c) 133 | self.color = color 134 | 135 | def DE(self, p): 136 | r = get_global(self.r) 137 | n = get_global(self.n) 138 | c = get_global(self.c) 139 | q = p[:3] - c 140 | return (np.linalg.norm(q - n*np.dot(n,q)) - r) / p[3] 141 | 142 | def NP(self, p): 143 | r = get_global(self.r) 144 | c = get_global(self.c) 145 | n = p[:3] - c 146 | m_ix = (0 if abs(n[0]) > abs(n[1]) else 1) 147 | n[m_ix] = 0.0 148 | n = normalize(n) * r 149 | n[m_ix] = p[m_ix] - c[m_ix] 150 | return n + c 151 | 152 | def glsl(self): 153 | return 'de_inf_line(p' + cond_offset(self.c) + ', ' + vec3_str(self.n) + ', ' + float_str(self.r) + ')' 154 | 155 | def glsl_col(self): 156 | return make_color(self) 157 | 158 | class XPlane: 159 | def __init__(self, x=0.0, color=(1,1,1)): 160 | self.x = set_global_float(x) 161 | self.color = color 162 | 163 | def DE(self, p): 164 | x = get_global(self.x) 165 | return abs(p[0] - x) / p[3] 166 | 167 | def NP(self, p): 168 | x = get_global(self.x) 169 | return np.array([x, p[1], p[2]]) 170 | 171 | def glsl(self): 172 | return 'abs(p.x' + cond_subtract(self.x) + ') / p.w' 173 | 174 | def glsl_col(self): 175 | return make_color(self) 176 | 177 | class YPlane: 178 | def __init__(self, x=0.0, color=(1,1,1)): 179 | self.x = set_global_float(x) 180 | self.color = color 181 | 182 | def DE(self, p): 183 | x = get_global(self.x) 184 | return abs(p[1] - x) / p[3] 185 | 186 | def NP(self, p): 187 | x = get_global(self.x) 188 | return np.array([p[0], x, p[2]]) 189 | 190 | def glsl(self): 191 | return 'abs(p.y' + cond_subtract(self.x) + ') / p.w' 192 | 193 | def glsl_col(self): 194 | return make_color(self) 195 | 196 | class ZPlane: 197 | def __init__(self, x=0.0, color=(1,1,1)): 198 | self.x = set_global_float(x) 199 | self.color = color 200 | 201 | def DE(self, p): 202 | x = get_global(self.x) 203 | return abs(p[2] - x) / p[3] 204 | 205 | def NP(self, p): 206 | x = get_global(self.x) 207 | return np.array([p[0], p[1], x]) 208 | 209 | def glsl(self): 210 | return 'abs(p.z' + cond_subtract(self.x) + ') / p.w' 211 | 212 | def glsl_col(self): 213 | return make_color(self) 214 | 215 | class XHalfSpace: 216 | def __init__(self, x=0.0, color=(1,1,1)): 217 | self.x = set_global_float(x) 218 | self.color = color 219 | 220 | def DE(self, p): 221 | x = get_global(self.x) 222 | return (p[0] - x) / p[3] 223 | 224 | def NP(self, p): 225 | x = get_global(self.x) 226 | return np.array([x, p[1], p[2]]) 227 | 228 | def glsl(self): 229 | return '(p.x' + cond_subtract(self.x) + ') / p.w' 230 | 231 | def glsl_col(self): 232 | return make_color(self) 233 | 234 | class YHalfSpace: 235 | def __init__(self, x=0.0, color=(1,1,1)): 236 | self.x = set_global_float(x) 237 | self.color = color 238 | 239 | def DE(self, p): 240 | x = get_global(self.x) 241 | return (p[1] - x) / p[3] 242 | 243 | def NP(self, p): 244 | x = get_global(self.x) 245 | return np.array([p[0], x, p[2]]) 246 | 247 | def glsl(self): 248 | return '(p.y' + cond_subtract(self.x) + ') / p.w' 249 | 250 | def glsl_col(self): 251 | return make_color(self) 252 | 253 | class ZHalfSpace: 254 | def __init__(self, x=0.0, color=(1,1,1)): 255 | self.x = set_global_float(x) 256 | self.color = color 257 | 258 | def DE(self, p): 259 | x = get_global(self.x) 260 | return (p[2] - x) / p[3] 261 | 262 | def NP(self, p): 263 | x = get_global(self.x) 264 | return np.array([p[0], p[1], x]) 265 | 266 | def glsl(self): 267 | return '(p.z' + cond_subtract(self.x) + ') / p.w' 268 | 269 | def glsl_col(self): 270 | return make_color(self) 271 | -------------------------------------------------------------------------------- /pyspace/object.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .util import * 3 | 4 | class Object: 5 | def __init__(self): 6 | self.trans = [] 7 | self.name = 'obj' + str(id(self)) 8 | 9 | def add(self, fold): 10 | self.trans.append(fold) 11 | 12 | def DE(self, origin): 13 | p = np.copy(origin) 14 | d = 1e20 15 | for t in self.trans: 16 | if hasattr(t, 'fold'): 17 | if hasattr(t, 'o'): 18 | t.o = origin 19 | t.fold(p) 20 | elif hasattr(t, 'DE'): 21 | d = min(d, t.DE(p)) 22 | elif hasattr(t, 'orbit'): pass 23 | else: 24 | raise Exception("Invalid type in transformation queue") 25 | return d 26 | 27 | def NP(self, origin): 28 | undo = [] 29 | p = np.copy(origin) 30 | d = 1e20 31 | n = np.zeros((3,), dtype=np.float32) 32 | for t in self.trans: 33 | undo.append((t, np.copy(p))) 34 | if hasattr(t, 'fold'): 35 | if hasattr(fold, 'o'): 36 | fold.o = origin 37 | t.fold(p) 38 | elif hasattr(t, 'NP'): pass 39 | elif hasattr(t, 'orbit'): pass 40 | else: 41 | raise Exception("Invalid type in transformation queue") 42 | for t, p in undo[::-1]: 43 | if hasattr(t, 'fold'): 44 | t.unfold(p, n) 45 | elif hasattr(t, 'NP'): 46 | cur_n = t.NP(p) 47 | cur_d = norm_sq(cur_n - p[:3]) / (p[3] * p[3]) 48 | if cur_d < d: 49 | d = cur_d 50 | n = cur_n 51 | return n 52 | 53 | def glsl(self): 54 | return 'de_' + self.name + '(p)' 55 | 56 | def glsl_col(self): 57 | return 'col_' + self.name + '(p)' 58 | 59 | def forwared_decl(self): 60 | s = 'float de_' + self.name + '(vec4 p);\n' 61 | s += 'vec4 col_' + self.name + '(vec4 p);\n' 62 | return s 63 | 64 | def compiled(self, nested_refs): 65 | new_refs = [] 66 | s = 'float de_' + self.name + '(vec4 p) {\n' 67 | s += '\tvec4 o = p;\n' 68 | s += '\tfloat d = 1e20;\n' 69 | for t in self.trans: 70 | if hasattr(t, 'fold'): 71 | s += t.glsl() 72 | elif hasattr(t, 'DE'): 73 | s += '\td = min(d, ' + t.glsl() + ');\n' 74 | if hasattr(t, 'forwared_decl') and t.name not in nested_refs: 75 | nested_refs[t.name] = t 76 | new_refs.append(t) 77 | elif hasattr(t, 'orbit'): pass 78 | else: 79 | raise Exception("Invalid type in transformation queue") 80 | s += '\treturn d;\n' 81 | s += '}\n' 82 | s += 'vec4 col_' + self.name + '(vec4 p) {\n' 83 | s += '\tvec4 o = p;\n' 84 | s += '\tvec4 col = vec4(1e20);\n' 85 | s += '\tvec4 newCol;\n' 86 | for t in self.trans: 87 | if hasattr(t, 'fold'): 88 | s += t.glsl() 89 | elif hasattr(t, 'DE'): 90 | s += '\tnewCol = ' + t.glsl_col() + ';\n' 91 | s += '\tif (newCol.w < col.w) { col = newCol; }\n' 92 | elif hasattr(t, 'orbit'): 93 | s += t.orbit() 94 | s += '\treturn col;\n' 95 | s += '}\n' 96 | for obj in new_refs: 97 | s += obj.compiled(nested_refs) 98 | return s 99 | -------------------------------------------------------------------------------- /pyspace/shader.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from OpenGL.GL import * 3 | from .util import _PYSPACE_GLOBAL_VARS, to_vec3, to_str 4 | from . import camera 5 | import os 6 | 7 | class Shader: 8 | def __init__(self, obj): 9 | self.obj = obj 10 | self.keys = {} 11 | 12 | def set(self, key, val): 13 | if key in _PYSPACE_GLOBAL_VARS: 14 | cur_val = _PYSPACE_GLOBAL_VARS[key] 15 | if type(val) is float and type(cur_val) is not float: 16 | val = to_vec3(val) 17 | _PYSPACE_GLOBAL_VARS[key] = val 18 | if key in self.keys: 19 | key_id = self.keys[key] 20 | if type(val) is float: 21 | glUniform1f(key_id, val) 22 | else: 23 | glUniform3fv(key_id, 1, val) 24 | 25 | def get(self, key): 26 | if key in _PYSPACE_GLOBAL_VARS: 27 | return _PYSPACE_GLOBAL_VARS[key] 28 | return None 29 | 30 | def compile(self, cam): 31 | #Open the shader source 32 | vert_dir = os.path.join(os.path.dirname(__file__), 'vert.glsl') 33 | frag_dir = os.path.join(os.path.dirname(__file__), 'frag.glsl') 34 | v_shader = open(vert_dir).read() 35 | f_shader = open(frag_dir).read() 36 | 37 | #Create code for all defines 38 | define_code = '' 39 | for k in cam.params: 40 | param = to_str(cam.params[k]) 41 | define_code += '#define ' + k + ' ' + param + '\n' 42 | define_code += '#define DE de_' + self.obj.name + '\n' 43 | define_code += '#define COL col_' + self.obj.name + '\n' 44 | split_ix = f_shader.index('// [/pydefine]') 45 | f_shader = f_shader[:split_ix] + define_code + f_shader[split_ix:] 46 | 47 | #Create code for all keys 48 | var_code = '' 49 | for k in _PYSPACE_GLOBAL_VARS: 50 | typ = 'float' if type(_PYSPACE_GLOBAL_VARS[k]) is float else 'vec3' 51 | var_code += 'uniform ' + typ + ' _' + k + ';\n' 52 | 53 | split_ix = f_shader.index('// [/pyvars]') 54 | f_shader = f_shader[:split_ix] + var_code + f_shader[split_ix:] 55 | 56 | #Create code for all pyspace 57 | nested_refs = {} 58 | space_code = self.obj.compiled(nested_refs) 59 | 60 | #Also add forward declarations 61 | forwared_decl_code = '' 62 | for k in nested_refs: 63 | forwared_decl_code += nested_refs[k].forwared_decl() 64 | space_code = forwared_decl_code + space_code 65 | 66 | split_ix = f_shader.index('// [/pyspace]') 67 | f_shader = f_shader[:split_ix] + space_code + f_shader[split_ix:] 68 | 69 | #Debugging the shader 70 | #open('frag_gen.glsl', 'w').write(f_shader) 71 | 72 | #Compile program 73 | program = self.compile_program(v_shader, f_shader) 74 | 75 | #Get variable ids for each uniform 76 | for k in _PYSPACE_GLOBAL_VARS: 77 | self.keys[k] = glGetUniformLocation(program, '_' + k); 78 | 79 | #Return the program 80 | return program 81 | 82 | def compile_shader(self, source, shader_type): 83 | shader = glCreateShader(shader_type) 84 | glShaderSource(shader, source) 85 | glCompileShader(shader) 86 | 87 | status = c_int() 88 | glGetShaderiv(shader, GL_COMPILE_STATUS, byref(status)) 89 | if not status.value: 90 | self.print_log(shader) 91 | glDeleteShader(shader) 92 | raise ValueError('Shader compilation failed') 93 | return shader 94 | 95 | def compile_program(self, vertex_source, fragment_source): 96 | vertex_shader = None 97 | fragment_shader = None 98 | program = glCreateProgram() 99 | 100 | if vertex_source: 101 | print("Compiling Vertex Shader...") 102 | vertex_shader = self.compile_shader(vertex_source, GL_VERTEX_SHADER) 103 | glAttachShader(program, vertex_shader) 104 | if fragment_source: 105 | print("Compiling Fragment Shader...") 106 | fragment_shader = self.compile_shader(fragment_source, GL_FRAGMENT_SHADER) 107 | glAttachShader(program, fragment_shader) 108 | 109 | glBindAttribLocation(program, 0, "vPosition") 110 | glLinkProgram(program) 111 | 112 | if vertex_shader: 113 | glDeleteShader(vertex_shader) 114 | if fragment_shader: 115 | glDeleteShader(fragment_shader) 116 | 117 | return program 118 | 119 | def print_log(self, shader): 120 | length = c_int() 121 | glGetShaderiv(shader, GL_INFO_LOG_LENGTH, byref(length)) 122 | 123 | if length.value > 0: 124 | log = create_string_buffer(length.value) 125 | print(glGetShaderInfoLog(shader)) 126 | -------------------------------------------------------------------------------- /pyspace/util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def normalize(x): 4 | return x / np.linalg.norm(x) 5 | 6 | def norm_sq(v): 7 | return np.dot(v,v) 8 | 9 | def norm(v): 10 | return np.linalg.norm(v) 11 | 12 | def get_sub_keys(v): 13 | if type(v) is not tuple and type(v) is not list: 14 | return [] 15 | return [k for k in v if type(k) is str] 16 | 17 | def to_vec3(v): 18 | if isinstance(v, (float, int)): 19 | return np.array([v, v, v], dtype=np.float32) 20 | elif len(get_sub_keys(v)) > 0: 21 | return v 22 | else: 23 | return np.array([v[0], v[1], v[2]], dtype=np.float32) 24 | 25 | def to_str(x): 26 | if type(x) is bool: 27 | return "1" if x else "0" 28 | elif isinstance(x, (list, tuple)): 29 | return vec3_str(x) 30 | else: 31 | return str(x) 32 | 33 | def float_str(x): 34 | if type(x) is str: 35 | return '_' + x 36 | else: 37 | return str(x) 38 | 39 | def vec3_str(v): 40 | if type(v) is str: 41 | return '_' + v 42 | elif isinstance(v, (float, int)): 43 | return 'vec3(' + str(v) + ')' 44 | else: 45 | return 'vec3(' + float_str(v[0]) + ',' + float_str(v[1]) + ',' + float_str(v[2]) + ')' 46 | 47 | def vec3_eq(v, val): 48 | if type(v) is str: 49 | return False 50 | for i in range(3): 51 | if v[i] != val[i]: 52 | return False 53 | return True 54 | 55 | def smin(a, b, k): 56 | h = min(max(0.5 + 0.5*(b - a)/k, 0.0), 1.0) 57 | return b*(1 - h) + a*h - k*h*(1.0 - h) 58 | 59 | def get_global(k): 60 | if type(k) is str: 61 | return _PYSPACE_GLOBAL_VARS[k] 62 | elif type(k) is tuple or type(k) is list: 63 | return np.array([get_global(i) for i in k], dtype=np.float32) 64 | else: 65 | return k 66 | 67 | def set_global_float(k): 68 | if type(k) is str: 69 | _PYSPACE_GLOBAL_VARS[k] = 0.0 70 | return k 71 | 72 | def set_global_vec3(k): 73 | if type(k) is str: 74 | _PYSPACE_GLOBAL_VARS[k] = to_vec3((0,0,0)) 75 | return k 76 | elif isinstance(k, (float, int)): 77 | return to_vec3(k) 78 | else: 79 | sk = get_sub_keys(k) 80 | for i in sk: 81 | _PYSPACE_GLOBAL_VARS[i] = 0.0 82 | return to_vec3(k) 83 | 84 | def cond_offset(p): 85 | if type(p) is str or np.count_nonzero(p) > 0: 86 | return ' - vec4(' + vec3_str(p) + ', 0)' 87 | return '' 88 | 89 | def cond_subtract(p): 90 | if type(p) is str or p > 0: 91 | return ' - ' + float_str(p) 92 | return '' 93 | 94 | def make_color(geo): 95 | if type(geo.color) is tuple or type(geo.color) is np.ndarray: 96 | return 'vec4(' + vec3_str(geo.color) + ', ' + geo.glsl() + ')' 97 | elif geo.color == 'orbit' or geo.color == 'o': 98 | return 'vec4(orbit, ' + geo.glsl() + ')' 99 | else: 100 | raise Exception("Invalid coloring type") 101 | 102 | _PYSPACE_GLOBAL_VARS = {} 103 | -------------------------------------------------------------------------------- /pyspace/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 120 2 | attribute vec4 vPosition; 3 | void main() { 4 | gl_Position = vPosition; 5 | } 6 | -------------------------------------------------------------------------------- /ray_marcher_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pygame, sys, math, random, os 4 | import numpy as np 5 | import pyspace 6 | 7 | from pyspace.coloring import * 8 | from pyspace.fold import * 9 | from pyspace.geo import * 10 | from pyspace.object import * 11 | from pyspace.shader import Shader 12 | from pyspace.camera import Camera 13 | 14 | from ctypes import * 15 | from OpenGL.GL import * 16 | from pygame.locals import * 17 | 18 | import os 19 | os.environ['SDL_VIDEO_CENTERED'] = '1' 20 | 21 | #Size of the window and rendering 22 | win_size = (1280, 720) 23 | 24 | #Maximum frames per second 25 | max_fps = 30 26 | 27 | #Forces an 'up' orientation when True, free-camera when False 28 | gimbal_lock = False 29 | 30 | #Mouse look speed 31 | look_speed = 0.003 32 | 33 | #Use this avoids collisions with the fractal 34 | auto_velocity = True 35 | auto_multiplier = 2.0 36 | 37 | #Maximum velocity of the camera 38 | max_velocity = 2.0 39 | 40 | #Amount of acceleration when moving 41 | speed_accel = 2.0 42 | 43 | #Velocity decay factor when keys are released 44 | speed_decel = 0.6 45 | 46 | clicking = False 47 | mouse_pos = None 48 | screen_center = (win_size[0]/2, win_size[1]/2) 49 | start_pos = [0, 0, 12.0] 50 | vel = np.zeros((3,), dtype=np.float32) 51 | look_x = 0.0 52 | look_y = 0.0 53 | 54 | #---------------------------------------------- 55 | # When building your own fractals, you can 56 | # substitute numbers with string placeholders 57 | # which can be tuned real-time with key bindings. 58 | # 59 | # In this example program: 60 | # '0' +Insert -Delete 61 | # '1' +Home -End 62 | # '2' +PageUp -PageDown 63 | # '3' +NumPad7 -NumPad4 64 | # '4' +NumPad8 -NumPad5 65 | # '5' +NumPad9 -NumPad6 66 | # 67 | # Hold down left-shift to decrease rate 10x 68 | # Hold down right-shift to increase rate 10x 69 | # 70 | # Set initial values of '0' through '6' below 71 | #---------------------------------------------- 72 | keyvars = [1.5, 1.5, 2.0, 1.0, 1.0, 1.0] 73 | 74 | #---------------------------------------------- 75 | # Fractal Examples Below 76 | #---------------------------------------------- 77 | def infinite_spheres(): 78 | obj = Object() 79 | obj.add(FoldRepeatX(2.0)) 80 | obj.add(FoldRepeatY(2.0)) 81 | obj.add(FoldRepeatZ(2.0)) 82 | obj.add(Sphere(0.5, (1.0, 1.0, 1.0), color=(0.9,0.9,0.5))) 83 | return obj 84 | 85 | def butterweed_hills(): 86 | obj = Object() 87 | obj.add(OrbitInitZero()) 88 | for _ in range(30): 89 | obj.add(FoldAbs()) 90 | obj.add(FoldScaleTranslate(1.5, (-1.0,-0.5,-0.2))) 91 | obj.add(OrbitSum((0.5, 0.03, 0.0))) 92 | obj.add(FoldRotateX(3.61)) 93 | obj.add(FoldRotateY(2.03)) 94 | obj.add(Sphere(1.0, color='orbit')) 95 | return obj 96 | 97 | def mandelbox(): 98 | obj = Object() 99 | obj.add(OrbitInitInf()) 100 | for _ in range(16): 101 | obj.add(FoldBox(1.0)) 102 | obj.add(FoldSphere(0.5, 1.0)) 103 | obj.add(FoldScaleOrigin(2.0)) 104 | obj.add(OrbitMinAbs(1.0)) 105 | obj.add(Box(6.0, color='orbit')) 106 | return obj 107 | 108 | def mausoleum(): 109 | obj = Object() 110 | obj.add(OrbitInitZero()) 111 | for _ in range(8): 112 | obj.add(FoldBox(0.34)) 113 | obj.add(FoldMenger()) 114 | obj.add(FoldScaleTranslate(3.28, (-5.27,-0.34,0.0))) 115 | obj.add(FoldRotateX(math.pi/2)) 116 | obj.add(OrbitMax((0.42,0.38,0.19))) 117 | obj.add(Box(2.0, color='orbit')) 118 | return obj 119 | 120 | def menger(): 121 | obj = Object() 122 | for _ in range(8): 123 | obj.add(FoldAbs()) 124 | obj.add(FoldMenger()) 125 | obj.add(FoldScaleTranslate(3.0, (-2,-2,0))) 126 | obj.add(FoldPlane((0,0,-1), -1)) 127 | obj.add(Box(2.0, color=(.2,.5,1))) 128 | return obj 129 | 130 | def tree_planet(): 131 | obj = Object() 132 | obj.add(OrbitInitInf()) 133 | for _ in range(30): 134 | obj.add(FoldRotateY(0.44)) 135 | obj.add(FoldAbs()) 136 | obj.add(FoldMenger()) 137 | obj.add(OrbitMinAbs((0.24,2.28,7.6))) 138 | obj.add(FoldScaleTranslate(1.3, (-2,-4.8,0))) 139 | obj.add(FoldPlane((0,0,-1), 0)) 140 | obj.add(Box(4.8, color='orbit')) 141 | return obj 142 | 143 | def sierpinski_tetrahedron(): 144 | obj = Object() 145 | obj.add(OrbitInitZero()) 146 | for _ in range(9): 147 | obj.add(FoldSierpinski()) 148 | obj.add(FoldScaleTranslate(2, -1)) 149 | obj.add(Tetrahedron(color=(0.8,0.8,0.5))) 150 | return obj 151 | 152 | def snow_stadium(): 153 | obj = Object() 154 | obj.add(OrbitInitInf()) 155 | for _ in range(30): 156 | obj.add(FoldRotateY(3.33)) 157 | obj.add(FoldSierpinski()) 158 | obj.add(FoldRotateX(0.15)) 159 | obj.add(FoldMenger()) 160 | obj.add(FoldScaleTranslate(1.57, (-6.61, -4.0, -2.42))) 161 | obj.add(OrbitMinAbs(1.0)) 162 | obj.add(Box(4.8, color='orbit')) 163 | return obj 164 | 165 | def test_fractal(): 166 | obj = Object() 167 | obj.add(OrbitInitInf()) 168 | for _ in range(20): 169 | obj.add(FoldSierpinski()) 170 | obj.add(FoldMenger()) 171 | obj.add(FoldRotateY(math.pi/2)) 172 | obj.add(FoldAbs()) 173 | obj.add(FoldRotateZ('0')) 174 | obj.add(FoldScaleTranslate(1.89, (-7.10, 0.396, -6.29))) 175 | obj.add(OrbitMinAbs((1,1,1))) 176 | obj.add(Box(6.0, color='orbit')) 177 | return obj 178 | 179 | #---------------------------------------------- 180 | # Helper Utilities 181 | #---------------------------------------------- 182 | def interp_data(x, f=2.0): 183 | new_dim = int(x.shape[0]*f) 184 | output = np.empty((new_dim,) + x.shape[1:], dtype=np.float32) 185 | for i in range(new_dim): 186 | a, b1 = math.modf(float(i) / f) 187 | b2 = min(b1 + 1, x.shape[0] - 1) 188 | output[i] = x[int(b1)]*(1-a) + x[int(b2)]*a 189 | return output 190 | 191 | def make_rot(angle, axis_ix): 192 | s = math.sin(angle) 193 | c = math.cos(angle) 194 | if axis_ix == 0: 195 | return np.array([[ 1, 0, 0], 196 | [ 0, c, -s], 197 | [ 0, s, c]], dtype=np.float32) 198 | elif axis_ix == 1: 199 | return np.array([[ c, 0, s], 200 | [ 0, 1, 0], 201 | [-s, 0, c]], dtype=np.float32) 202 | elif axis_ix == 2: 203 | return np.array([[ c, -s, 0], 204 | [ s, c, 0], 205 | [ 0, 0, 1]], dtype=np.float32) 206 | 207 | def reorthogonalize(mat): 208 | u, s, v = np.linalg.svd(mat) 209 | return np.dot(u, v) 210 | 211 | # move the cursor back , only if the window is focused 212 | def center_mouse(): 213 | if pygame.key.get_focused(): 214 | pygame.mouse.set_pos(screen_center) 215 | 216 | #-------------------------------------------------- 217 | # Video Recording 218 | # 219 | # When you're ready to record a video, press 'r' 220 | # to start recording, and then move around. The 221 | # camera's path and live '0' through '5' parameters 222 | # are recorded to a file. Press 'r' when finished. 223 | # 224 | # Now you can exit the program and turn up the 225 | # camera parameters for better rendering. For 226 | # example; window size, anti-aliasing, motion blur, 227 | # and depth of field are great options. 228 | # 229 | # When you're ready to playback and render, press 230 | # 'p' and the recorded movements are played back. 231 | # Images are saved to a './playback' folder. You 232 | # can import the image sequence to editing software 233 | # to convert it to a video. 234 | # 235 | # You can press 's' anytime for a screenshot. 236 | #--------------------------------------------------- 237 | 238 | if __name__ == '__main__': 239 | pygame.init() 240 | window = pygame.display.set_mode(win_size, OPENGL | DOUBLEBUF) 241 | pygame.mouse.set_visible(False) 242 | center_mouse() 243 | 244 | #====================================================== 245 | # Change the fractal here 246 | #====================================================== 247 | obj_render = tree_planet() 248 | #====================================================== 249 | 250 | #====================================================== 251 | # Change camera settings here 252 | # See pyspace/camera.py for all camera options 253 | #====================================================== 254 | camera = Camera() 255 | camera['ANTIALIASING_SAMPLES'] = 1 256 | camera['AMBIENT_OCCLUSION_STRENGTH'] = 0.01 257 | #====================================================== 258 | 259 | shader = Shader(obj_render) 260 | program = shader.compile(camera) 261 | print("Compiled!") 262 | 263 | matID = glGetUniformLocation(program, "iMat") 264 | prevMatID = glGetUniformLocation(program, "iPrevMat") 265 | resID = glGetUniformLocation(program, "iResolution") 266 | ipdID = glGetUniformLocation(program, "iIPD") 267 | 268 | glUseProgram(program) 269 | glUniform2fv(resID, 1, win_size) 270 | glUniform1f(ipdID, 0.04) 271 | 272 | fullscreen_quad = np.array([-1.0, -1.0, 0.0, 1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 0.0], dtype=np.float32) 273 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, fullscreen_quad) 274 | glEnableVertexAttribArray(0) 275 | 276 | mat = np.identity(4, np.float32) 277 | mat[3,:3] = np.array(start_pos) 278 | prevMat = np.copy(mat) 279 | for i in range(len(keyvars)): 280 | shader.set(str(i), keyvars[i]) 281 | 282 | recording = None 283 | rec_vars = None 284 | playback = None 285 | playback_vars = None 286 | playback_ix = -1 287 | frame_num = 0 288 | 289 | def start_playback(): 290 | global playback 291 | global playback_vars 292 | global playback_ix 293 | global prevMat 294 | if not os.path.exists('playback'): 295 | os.makedirs('playback') 296 | playback = np.load('recording.npy') 297 | playback_vars = np.load('rec_vars.npy') 298 | playback = interp_data(playback, 2) 299 | playback_vars = interp_data(playback_vars, 2) 300 | playback_ix = 0 301 | prevMat = playback[0] 302 | 303 | clock = pygame.time.Clock() 304 | while True: 305 | for event in pygame.event.get(): 306 | if event.type == pygame.QUIT: 307 | sys.exit(0) 308 | elif event.type == pygame.KEYDOWN: 309 | if event.key == pygame.K_r: 310 | if recording is None: 311 | print("Recording...") 312 | recording = [] 313 | rec_vars = [] 314 | else: 315 | np.save('recording.npy', np.array(recording, dtype=np.float32)) 316 | np.save('rec_vars.npy', np.array(rec_vars, dtype=np.float32)) 317 | recording = None 318 | rec_vars = None 319 | print("Finished Recording.") 320 | elif event.key == pygame.K_p: 321 | start_playback() 322 | elif event.key == pygame.K_c: 323 | pygame.image.save(window, 'screenshot.png') 324 | elif event.key == pygame.K_ESCAPE: 325 | sys.exit(0) 326 | 327 | mat[3,:3] += vel * (clock.get_time() / 1000) 328 | 329 | if auto_velocity: 330 | de = obj_render.DE(mat[3]) * auto_multiplier 331 | if not np.isfinite(de): 332 | de = 0.0 333 | else: 334 | de = 1e20 335 | 336 | all_keys = pygame.key.get_pressed() 337 | 338 | rate = 0.01 339 | if all_keys[pygame.K_LSHIFT]: rate *= 0.1 340 | elif all_keys[pygame.K_RSHIFT]: rate *= 10.0 341 | 342 | if all_keys[pygame.K_INSERT]: keyvars[0] += rate; print(keyvars) 343 | if all_keys[pygame.K_DELETE]: keyvars[0] -= rate; print(keyvars) 344 | if all_keys[pygame.K_HOME]: keyvars[1] += rate; print(keyvars) 345 | if all_keys[pygame.K_END]: keyvars[1] -= rate; print(keyvars) 346 | if all_keys[pygame.K_PAGEUP]: keyvars[2] += rate; print(keyvars) 347 | if all_keys[pygame.K_PAGEDOWN]: keyvars[2] -= rate; print(keyvars) 348 | if all_keys[pygame.K_KP7]: keyvars[3] += rate; print(keyvars) 349 | if all_keys[pygame.K_KP4]: keyvars[3] -= rate; print(keyvars) 350 | if all_keys[pygame.K_KP8]: keyvars[4] += rate; print(keyvars) 351 | if all_keys[pygame.K_KP5]: keyvars[4] -= rate; print(keyvars) 352 | if all_keys[pygame.K_KP9]: keyvars[5] += rate; print(keyvars) 353 | if all_keys[pygame.K_KP6]: keyvars[5] -= rate; print(keyvars) 354 | 355 | if playback is None: 356 | prev_mouse_pos = mouse_pos 357 | mouse_pos = pygame.mouse.get_pos() 358 | dx,dy = 0,0 359 | if prev_mouse_pos is not None: 360 | center_mouse() 361 | time_rate = (clock.get_time() / 1000.0) / (1 / max_fps) 362 | dx = (mouse_pos[0] - screen_center[0]) * time_rate 363 | dy = (mouse_pos[1] - screen_center[1]) * time_rate 364 | 365 | if pygame.key.get_focused(): 366 | if gimbal_lock: 367 | look_x += dx * look_speed 368 | look_y += dy * look_speed 369 | look_y = min(max(look_y, -math.pi/2), math.pi/2) 370 | 371 | rx = make_rot(look_x, 1) 372 | ry = make_rot(look_y, 0) 373 | 374 | mat[:3,:3] = np.dot(ry, rx) 375 | else: 376 | rx = make_rot(dx * look_speed, 1) 377 | ry = make_rot(dy * look_speed, 0) 378 | 379 | mat[:3,:3] = np.dot(ry, np.dot(rx, mat[:3,:3])) 380 | mat[:3,:3] = reorthogonalize(mat[:3,:3]) 381 | 382 | acc = np.zeros((3,), dtype=np.float32) 383 | if all_keys[pygame.K_a]: 384 | acc[0] -= speed_accel / max_fps 385 | if all_keys[pygame.K_d]: 386 | acc[0] += speed_accel / max_fps 387 | if all_keys[pygame.K_w]: 388 | acc[2] -= speed_accel / max_fps 389 | if all_keys[pygame.K_s]: 390 | acc[2] += speed_accel / max_fps 391 | 392 | if np.dot(acc, acc) == 0.0: 393 | vel *= speed_decel # TODO 394 | else: 395 | vel += np.dot(mat[:3,:3].T, acc) 396 | vel_ratio = min(max_velocity, de) / (np.linalg.norm(vel) + 1e-12) 397 | if vel_ratio < 1.0: 398 | vel *= vel_ratio 399 | if all_keys[pygame.K_SPACE]: 400 | vel *= 10.0 401 | 402 | if recording is not None: 403 | recording.append(np.copy(mat)) 404 | rec_vars.append(np.array(keyvars, dtype=np.float32)) 405 | else: 406 | if playback_ix >= 0: 407 | ix_str = '%04d' % playback_ix 408 | pygame.image.save(window, 'playback/frame' + ix_str + '.png') 409 | if playback_ix >= playback.shape[0]: 410 | playback = None 411 | break 412 | else: 413 | mat = prevMat * 0.98 + playback[playback_ix] * 0.02 414 | mat[:3,:3] = reorthogonalize(mat[:3,:3]) 415 | keyvars = playback_vars[playback_ix].tolist() 416 | playback_ix += 1 417 | 418 | for i in range(3): 419 | shader.set(str(i), keyvars[i]) 420 | shader.set('v', np.array(keyvars[3:6])) 421 | shader.set('pos', mat[3,:3]) 422 | 423 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 424 | glUniformMatrix4fv(matID, 1, False, mat) 425 | glUniformMatrix4fv(prevMatID, 1, False, prevMat) 426 | prevMat = np.copy(mat) 427 | 428 | glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) 429 | pygame.display.flip() 430 | clock.tick(max_fps) 431 | frame_num += 1 432 | print(clock.get_fps()) 433 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyOpenGL>=3.1.0 2 | Pygame>=1.9.4 3 | numpy>=1.16.1 4 | --------------------------------------------------------------------------------