Compare commits

..

7 commits
v1.0 ... master

Author SHA1 Message Date
Quantum e77d4661cc Require pyglet<1.4 for now 2020-05-01 02:38:44 -04:00
Quantum 7cb34c9142 Release punyverse 1.2 with better cross platform support 2018-12-01 01:27:56 -05:00
Quantum e72a3bc623 Make sky disablable, and disable by default on macOS 2018-11-27 01:44:06 -05:00
Quantum 9cdf3b8513 Make punyverse runnable on macOS 2018-11-27 01:27:59 -05:00
Guanzhong Chen 885194c7a0
Update README.md 2018-09-26 11:06:23 -04:00
Quantum 8739120036 So much legacy texture stuff can go. 2018-08-30 16:47:10 -04:00
Quantum d5297d3619 Release 1.1 with higher resolution textures.
After all, it doesn't exactly make sense for saturn's moons and mercury to have more detailed textures than larger objects like the sun or venus.

Also used one single texture to do the transparency mask for the glow, instead of having separate textures for each glowy object.
2018-08-29 17:55:06 -04:00
28 changed files with 84 additions and 55 deletions

4
.gitignore vendored
View file

@ -62,3 +62,7 @@ library.zip
# Our special launcher # Our special launcher
!/punyverse/launcher.c !/punyverse/launcher.c
# macOS
.DS_Store
._.DS_Store

View file

@ -5,4 +5,5 @@ include punyverse/world.json
graft punyverse/assets graft punyverse/assets
include punyverse/shaders/*.glsl include punyverse/shaders/*.glsl
include punyverse/*.c include punyverse/*.c
include punyverse/*.h
exclude punyverse/*.pyx exclude punyverse/*.pyx

View file

@ -1,4 +1,4 @@
# punyverse [![Linux Build Status](https://img.shields.io/travis/quantum5/punyverse.svg?logo=linux)](https://travis-ci.org/quantum5/punyverse) [![Windows Build Status](https://img.shields.io/appveyor/ci/quantum5/punyverse.svg?logo=windows)](https://ci.appveyor.com/project/quantum5/punyverse) [![PyPI](https://img.shields.io/pypi/v/punyverse.svg)](https://pypi.org/project/punyverse/) [![PyPI - Format](https://img.shields.io/pypi/format/punyverse.svg)](https://pypi.org/project/punyverse/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/punyverse.svg)](https://pypi.org/project/punyverse/) # punyverse [![Linux Build Status](https://img.shields.io/travis/quantum5/punyverse.svg?logo=linux&logoColor=white)](https://travis-ci.org/quantum5/punyverse) [![Windows Build Status](https://img.shields.io/appveyor/ci/quantum5/punyverse.svg?logo=windows)](https://ci.appveyor.com/project/quantum5/punyverse) [![PyPI](https://img.shields.io/pypi/v/punyverse.svg)](https://pypi.org/project/punyverse/) [![PyPI - Format](https://img.shields.io/pypi/format/punyverse.svg)](https://pypi.org/project/punyverse/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/punyverse.svg)](https://pypi.org/project/punyverse/)
Python simulator of a puny universe. (How many words can I stick into one?) Python simulator of a puny universe. (How many words can I stick into one?)

View file

@ -1,8 +1,4 @@
IF UNAME_SYSNAME == "Windows": cdef extern from "glwrapper.h":
cdef extern from "windows.h":
pass
cdef extern from "GL/gl.h":
ctypedef unsigned int GLenum ctypedef unsigned int GLenum
ctypedef unsigned char GLboolean ctypedef unsigned char GLboolean
ctypedef unsigned int GLbitfield ctypedef unsigned int GLbitfield

View file

@ -1,7 +1,9 @@
# This file contains all the textures that is bigger than the minimum OpenGL texture size # This file contains all the textures that is bigger than the minimum OpenGL texture size
# and hence may need to be resized depending on the machine # and hence may need to be resized depending on the machine
sun.jpg
mercury.jpg mercury.jpg
venus.jpg
earth.jpg earth.jpg
earth_normal.jpg earth_normal.jpg
earth_emission.jpg earth_emission.jpg
@ -11,6 +13,8 @@ moon.jpg
mars.jpg mars.jpg
jupiter.jpg jupiter.jpg
saturn.jpg saturn.jpg
uranus.jpg
neptune.jpg
sky_px.jpg sky_px.jpg
sky_py.jpg sky_py.jpg
sky_pz.jpg sky_pz.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 981 KiB

View file

@ -388,8 +388,9 @@ class SphericalBody(Body):
if 'atmosphere' in info: if 'atmosphere' in info:
atmosphere_data = info['atmosphere'] atmosphere_data = info['atmosphere']
atm_size = world.evaluate(atmosphere_data.get('diffuse_size', None)) atm_size = world.evaluate(atmosphere_data.get('glow_size', None))
atm_texture = atmosphere_data.get('diffuse_texture', None) atm_texture = atmosphere_data.get('glow_texture', None)
atm_color = atmosphere_data.get('glow_color', None)
cloud_texture = atmosphere_data.get('cloud_texture', None) cloud_texture = atmosphere_data.get('cloud_texture', None)
if cloud_texture is not None: if cloud_texture is not None:
self.cloud_transparency = get_best_texture(cloud_texture, loader=load_alpha_mask) self.cloud_transparency = get_best_texture(cloud_texture, loader=load_alpha_mask)
@ -405,8 +406,9 @@ class SphericalBody(Body):
self.clouds.stride, self.clouds.uv_offset) self.clouds.stride, self.clouds.uv_offset)
glBindBuffer(GL_ARRAY_BUFFER, 0) glBindBuffer(GL_ARRAY_BUFFER, 0)
if atm_texture is not None: if atm_texture is not None and atm_color is not None:
self.atm_texture = load_texture_1d(atm_texture, clamp=True) self.atm_texture = load_texture_1d(atm_texture, clamp=True)
self.atm_color = atm_color
self.atmosphere = Disk(self.radius, self.radius + atm_size, 30) self.atmosphere = Disk(self.radius, self.radius + atm_size, 30)
self.atmosphere_vao = VAO() self.atmosphere_vao = VAO()
shader = self.world.activate_shader('atmosphere') shader = self.world.activate_shader('atmosphere')
@ -513,7 +515,8 @@ class SphericalBody(Body):
shader.uniform_mat4('u_mvpMatrix', self.world.projection_matrix() * matrix) shader.uniform_mat4('u_mvpMatrix', self.world.projection_matrix() * matrix)
glBindTexture(GL_TEXTURE_1D, self.atm_texture) glBindTexture(GL_TEXTURE_1D, self.atm_texture)
shader.uniform_texture('u_texture', 0) shader.uniform_texture('u_transparency', 0)
shader.uniform_vec3('u_color', *self.atm_color)
with self.atmosphere_vao: with self.atmosphere_vao:
glDrawArrays(GL_TRIANGLE_STRIP, 0, self.atmosphere.vertex_count) glDrawArrays(GL_TRIANGLE_STRIP, 0, self.atmosphere.vertex_count)

9
punyverse/glwrapper.h Normal file
View file

@ -0,0 +1,9 @@
#ifdef _MSC_VER
# include <windows.h>
#endif
#ifdef __APPLE__
# include <OpenGL/gl.h>
#else
# include <GL/gl.h>
#endif

View file

@ -134,10 +134,10 @@ class LoaderWindow(pyglet.window.Window):
self.flip() self.flip()
self.dispatch_events() self.dispatch_events()
def load(self): def load(self, **kwargs):
start = time.clock() start = time.clock()
with glContext(self._main_context): with glContext(self._main_context):
world = World('world.json', self._load_callback) world = World('world.json', self._load_callback, **kwargs)
print('Loaded in %s seconds.' % (time.clock() - start)) print('Loaded in %s seconds.' % (time.clock() - start))
return world return world
@ -167,10 +167,10 @@ class LoaderConsole(object):
def _load_callback(self, phase, message, progress): def _load_callback(self, phase, message, progress):
print(message, file=self._output) print(message, file=self._output)
def load(self): def load(self, **kwargs):
start = time.clock() start = time.clock()
with glContext(self._main_context): with glContext(self._main_context):
world = World('world.json', self._load_callback) world = World('world.json', self._load_callback, **kwargs)
print('Loaded in %s seconds.' % (time.clock() - start), file=self._output) print('Loaded in %s seconds.' % (time.clock() - start), file=self._output)
return world return world

View file

@ -1,4 +1,5 @@
import argparse import argparse
import sys
import pyglet import pyglet
@ -8,9 +9,12 @@ DEBUG = False
def main(): def main():
macos = sys.platform == 'darwin'
parser = argparse.ArgumentParser(prog='punyverse', description=''' parser = argparse.ArgumentParser(prog='punyverse', description='''
Python simulator of a puny universe. Python simulator of a puny universe.
''') ''')
parser.set_defaults(sky=not macos)
parser.add_argument('-D', '--debug', help='Enable pyglet OpenGL debugging', action='store_true') parser.add_argument('-D', '--debug', help='Enable pyglet OpenGL debugging', action='store_true')
parser.add_argument('-d', '--high-depth', help='Use a larger depth buffer', parser.add_argument('-d', '--high-depth', help='Use a larger depth buffer',
const=32, default=24, dest='depth', nargs='?', type=int) const=32, default=24, dest='depth', nargs='?', type=int)
@ -20,14 +24,21 @@ def main():
action='store_false', dest='vsync') action='store_false', dest='vsync')
parser.add_argument('-n', '--normal', help='Enables the use of normal maps', parser.add_argument('-n', '--normal', help='Enables the use of normal maps',
action='store_true') action='store_true')
parser.add_argument('-s', '--sky', help='Enables the sky', dest='sky',
action='store_true')
parser.add_argument('-S', '--no-sky', help='Disables the sky', dest='sky',
action='store_false')
args = parser.parse_args() args = parser.parse_args()
versioning = dict(major_version=3, minor_version=3)
pyglet.options['debug_gl'] = args.debug pyglet.options['debug_gl'] = args.debug
if macos:
pyglet.options['shadow_window'] = False
versioning = dict(major_version=4, minor_version=1, forward_compatible=True)
template = pyglet.gl.Config(depth_size=args.depth, double_buffer=True, template = pyglet.gl.Config(depth_size=args.depth, double_buffer=True,
sample_buffers=args.multisample > 1, sample_buffers=args.multisample > 1,
samples=args.multisample, samples=args.multisample, **versioning)
major_version=3, minor_version=3)
platform = pyglet.window.get_platform() platform = pyglet.window.get_platform()
display = platform.get_default_display() display = platform.get_default_display()
@ -64,7 +75,7 @@ def main():
loader.context.set_current() loader.context.set_current()
loader.set_main_context(punyverse.context) loader.set_main_context(punyverse.context)
world = loader.load() world = loader.load(sky=args.sky)
punyverse.context.set_current() punyverse.context.set_current()
punyverse.initialize(world) punyverse.initialize(world)
loader.close() loader.close()

View file

@ -3,8 +3,10 @@
in float v_u; in float v_u;
out vec4 o_fragColor; out vec4 o_fragColor;
uniform sampler1D u_texture;
uniform vec3 u_color;
uniform sampler1D u_transparency;
void main() { void main() {
o_fragColor = texture(u_texture, v_u); o_fragColor = vec4(u_color, texture(u_transparency, v_u).r);
} }

View file

@ -34,7 +34,7 @@ void main() {
float diffuseIntensity = max(dot(v_normal, incident), 0.0); float diffuseIntensity = max(dot(v_normal, incident), 0.0);
float shininess = pow(max(dot(normalize(v_camDirection), reflected), 0), u_material.shininess); float shininess = pow(max(dot(normalize(v_camDirection), reflected), 0), u_material.shininess);
vec3 diffuse = u_material.hasDiffuse ? texture2D(u_material.diffuseMap, v_uv).rgb : vec3(1); vec3 diffuse = u_material.hasDiffuse ? texture(u_material.diffuseMap, v_uv).rgb : vec3(1);
vec3 ambient = u_material.ambient * u_sun.ambient * diffuse; vec3 ambient = u_material.ambient * u_sun.ambient * diffuse;
vec3 specular = u_material.specular * u_sun.specular * max(shininess, 0) * diffuseIntensity; vec3 specular = u_material.specular * u_sun.specular * max(shininess, 0) * diffuseIntensity;
diffuse *= u_material.diffuse * u_sun.diffuse * diffuseIntensity; diffuse *= u_material.diffuse * u_sun.diffuse * diffuseIntensity;

View file

@ -35,10 +35,10 @@ uniform Sun u_sun;
uniform Surface u_planet; uniform Surface u_planet;
void main() { void main() {
vec3 normal = u_planet.hasNormal ? normalize(v_TBN * texture2D(u_planet.normalMap, v_uv).rgb * 2 - 1) : v_normal; vec3 normal = u_planet.hasNormal ? normalize(v_TBN * texture(u_planet.normalMap, v_uv).rgb * 2 - 1) : v_normal;
vec3 diffuse = texture2D(u_planet.diffuseMap, v_uv).rgb; vec3 diffuse = texture(u_planet.diffuseMap, v_uv).rgb;
vec3 specular = u_planet.hasSpecular ? texture2D(u_planet.specularMap, v_uv).rgb : vec3(1); vec3 specular = u_planet.hasSpecular ? texture(u_planet.specularMap, v_uv).rgb : vec3(1);
vec3 emission = u_planet.hasEmission ? texture2D(u_planet.emissionMap, v_uv).rgb : vec3(1); vec3 emission = u_planet.hasEmission ? texture(u_planet.emissionMap, v_uv).rgb : vec3(1);
vec3 incident = normalize(u_sun.position - v_position); vec3 incident = normalize(u_sun.position - v_position);
vec3 reflected = normalize(reflect(-incident, normal)); vec3 reflected = normalize(reflect(-incident, normal));

View file

@ -131,10 +131,6 @@ def check_size(width, height):
if width > max_texture or height > max_texture: if width > max_texture or height > max_texture:
print('too large') print('too large')
raise ValueError('Texture too large') raise ValueError('Texture too large')
elif not gl_info.have_version(2) and not gl_info.have_extension('GL_ARB_texture_non_power_of_two'):
if not is_power2(width) or not is_power2(height):
print('not power of two')
raise ValueError('Texture not power of two')
def load_image(file, path): def load_image(file, path):
@ -217,12 +213,8 @@ def load_texture(file, clamp=False):
id = create_texture() id = create_texture()
glBindTexture(GL_TEXTURE_2D, id) glBindTexture(GL_TEXTURE_2D, id)
glTexImage2D(GL_TEXTURE_2D, 0, get_internal_mode(mode), width, height, 0, mode, GL_UNSIGNED_BYTE, texture)
if gl_info.have_version(3) or gl_info.have_extension('GL_ARB_framebuffer_object'): glGenerateMipmap(GL_TEXTURE_2D)
glTexImage2D(GL_TEXTURE_2D, 0, get_internal_mode(mode), width, height, 0, mode, GL_UNSIGNED_BYTE, texture)
glGenerateMipmap(GL_TEXTURE_2D)
else:
gluBuild2DMipmaps(GL_TEXTURE_2D, depth, width, height, mode, GL_UNSIGNED_BYTE, texture)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
@ -245,11 +237,8 @@ def load_texture_1d(file, clamp=False):
id = create_texture() id = create_texture()
glBindTexture(GL_TEXTURE_1D, id) glBindTexture(GL_TEXTURE_1D, id)
if gl_info.have_version(3) or gl_info.have_extension('GL_ARB_framebuffer_object'): glTexImage1D(GL_TEXTURE_1D, 0, get_internal_mode(mode), width, 0, mode, GL_UNSIGNED_BYTE, texture)
glTexImage1D(GL_TEXTURE_1D, 0, get_internal_mode(mode), width, 0, mode, GL_UNSIGNED_BYTE, texture) glGenerateMipmap(GL_TEXTURE_1D)
glGenerateMipmap(GL_TEXTURE_1D)
else:
gluBuild1DMipmaps(GL_TEXTURE_1D, depth, width, mode, GL_UNSIGNED_BYTE, texture)
if clamp: if clamp:
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
@ -272,12 +261,8 @@ def load_alpha_mask(file, clamp=False):
id = buffer.value id = buffer.value
glBindTexture(GL_TEXTURE_2D, id) glBindTexture(GL_TEXTURE_2D, id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, texture)
if gl_info.have_version(3) or gl_info.have_extension('GL_ARB_framebuffer_object'): glGenerateMipmap(GL_TEXTURE_2D)
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, texture)
glGenerateMipmap(GL_TEXTURE_2D)
else:
gluBuild2DMipmaps(GL_TEXTURE_2D, 1, width, height, GL_RED, GL_UNSIGNED_BYTE, texture)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)

View file

@ -199,6 +199,8 @@ class Punyverse(pyglet.window.Window):
if not width or not height: if not width or not height:
# Sometimes this happen for no reason? # Sometimes this happen for no reason?
return return
if hasattr(self, 'get_viewport_size'):
width, height = self.get_viewport_size()
glViewport(0, 0, width, height) glViewport(0, 0, width, height)
self.world.resize(width, height) self.world.resize(width, height)

View file

@ -7,7 +7,7 @@
"distance": "virtual distance to look better, in km", "distance": "virtual distance to look better, in km",
"sma": "semi-major axis used with mass of parent to calculate orbit, in km", "sma": "semi-major axis used with mass of parent to calculate orbit, in km",
"mass": "mass in kg", "mass": "mass in kg",
"texture": "a group of texture to use, tried in that order. a list means a colour", "texture": "a group of texture to use, tried in that order",
"model": "used to load a wavefront object instead of a textured sphere" "model": "used to load a wavefront object instead of a textured sphere"
}, },
"au": 10000, "au": 10000,
@ -24,8 +24,9 @@
"light_source": true, "light_source": true,
"type": "star", "type": "star",
"atmosphere": { "atmosphere": {
"diffuse_texture": "sun_diffuse.png", "glow_color": [0.92, 0.92, 0.82],
"diffuse_size": 300 "glow_texture": "glow.png",
"glow_size": 300
} }
}, },
"mercury": { "mercury": {
@ -61,8 +62,9 @@
"emission_map": ["earth_emission.jpg", "earth_emission_medium.jpg", "earth_emission_small.jpg"], "emission_map": ["earth_emission.jpg", "earth_emission_medium.jpg", "earth_emission_small.jpg"],
"atmosphere": { "atmosphere": {
"cloud_texture": ["cloudmap.jpg", "cloudmap_small.jpg"], "cloud_texture": ["cloudmap.jpg", "cloudmap_small.jpg"],
"diffuse_texture": "atmosphere_earth.png", "glow_color": [0.11, 0.32, 0.43],
"diffuse_size": 30 "glow_texture": "glow.png",
"glow_size": 30
}, },
"orbit_distance": "AU", "orbit_distance": "AU",
"satellites": { "satellites": {

View file

@ -30,7 +30,7 @@ class World(object):
'belt': ('belt.vertex.glsl', 'model.fragment.glsl'), 'belt': ('belt.vertex.glsl', 'model.fragment.glsl'),
} }
def __init__(self, file, callback): def __init__(self, file, callback, sky=True):
self.tracker = [] self.tracker = []
self.x = None self.x = None
self.y = None self.y = None
@ -40,6 +40,8 @@ class World(object):
self.asteroids = AsteroidManager(self) self.asteroids = AsteroidManager(self)
self.cam = Camera() self.cam = Camera()
self._sky = sky
self._program = None self._program = None
self.callback = callback self.callback = callback
self.programs = self._load_programs() self.programs = self._load_programs()
@ -127,7 +129,7 @@ class World(object):
'Loading %s.' % name, i / belt_count) 'Loading %s.' % name, i / belt_count)
self.tracker.append(Belt(name, self, info)) self.tracker.append(Belt(name, self, info))
if 'sky' in root: if 'sky' in root and self._sky:
def callback(index, file): def callback(index, file):
self.callback('Loading sky...', 'Loading %s.' % file, index / 6) self.callback('Loading sky...', 'Loading %s.' % file, index / 6)
self.tracker.append(Sky(self, root['sky'], callback)) self.tracker.append(Sky(self, root['sky'], callback))

View file

@ -26,9 +26,16 @@ else:
if os.name == 'nt': if os.name == 'nt':
gl_libs = ['opengl32'] gl_libs = ['opengl32']
elif sys.platform == 'darwin':
gl_libs = []
else: else:
gl_libs = ['GL'] gl_libs = ['GL']
if sys.platform == 'darwin':
extra_compile_args = extra_link_args = ['-framework', 'OpenGL']
else:
extra_compile_args = extra_link_args = []
with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f: with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f:
long_description = f.read() long_description = f.read()
@ -105,7 +112,7 @@ else:
setup( setup(
name='punyverse', name='punyverse',
version='1.0', version='1.2',
packages=['punyverse'], packages=['punyverse'],
package_data={ package_data={
'punyverse': [ 'punyverse': [
@ -124,7 +131,8 @@ setup(
], ],
}, },
ext_modules=cythonize([ ext_modules=cythonize([
Extension('punyverse._glgeom', sources=[pyx_path('punyverse/_glgeom.pyx')], libraries=gl_libs), Extension('punyverse._glgeom', sources=[pyx_path('punyverse/_glgeom.pyx')], libraries=gl_libs,
extra_compile_args=extra_compile_args, extra_link_args=extra_link_args),
]) + extra_libs, ]) + extra_libs,
cmdclass={'build_ext': build_ext}, cmdclass={'build_ext': build_ext},
@ -139,7 +147,7 @@ setup(
] ]
}, },
install_requires=['pyglet', 'Pillow', 'six'], install_requires=['pyglet<1.4', 'Pillow', 'six'],
author='quantum', author='quantum',
author_email='quantum2048@gmail.com', author_email='quantum2048@gmail.com',