Get networking working
This commit is contained in:
parent
49b8f97d6d
commit
809a577f6d
12
README.md
12
README.md
|
@ -1,2 +1,14 @@
|
|||
# axuy
|
||||
|
||||
Mininalist first-person shooter
|
||||
|
||||
## Install
|
||||
|
||||
The game is under heavy development. For contributors:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/McSinyx/axuy.git
|
||||
pip3 install -e axuy
|
||||
axuy --host localhost --port 12345 --width 1234 --height 567
|
||||
axuy --seeder=localhost:12345 --host localhost --port 8888 --width 1234 --height 567
|
||||
```
|
||||
|
|
96
axuy/main.py
96
axuy/main.py
|
@ -1,96 +0,0 @@
|
|||
# main.py - start game and main loop
|
||||
# Copyright (C) 2019 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of Axuy
|
||||
#
|
||||
# Axuy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Axuy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from random import shuffle
|
||||
|
||||
import glfw
|
||||
import moderngl
|
||||
|
||||
from .misc import color
|
||||
from .view import View
|
||||
|
||||
FOV_MIN = 30
|
||||
FOV_MAX = 120
|
||||
FOV_INIT = (FOV_MIN+FOV_MAX) / 2
|
||||
|
||||
|
||||
def main():
|
||||
"""Create window, OpenGL context and start main loop."""
|
||||
if not glfw.init():
|
||||
print('Failed to initialize glfw!')
|
||||
return
|
||||
|
||||
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
|
||||
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
|
||||
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
|
||||
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
|
||||
window = glfw.create_window(640, 480, 'Axuy', None, None)
|
||||
if not window:
|
||||
print('Failed to create glfw window!')
|
||||
glfw.terminate()
|
||||
return
|
||||
|
||||
glfw.make_context_current(window)
|
||||
glfw.swap_interval(1)
|
||||
glfw.set_input_mode(window, glfw.CURSOR, glfw.CURSOR_DISABLED)
|
||||
glfw.set_input_mode(window, glfw.STICKY_KEYS, True)
|
||||
|
||||
context = moderngl.create_context()
|
||||
context.enable(moderngl.BLEND)
|
||||
context.enable(moderngl.DEPTH_TEST)
|
||||
|
||||
fov = FOV_INIT
|
||||
def zoom(window, xoffset, yoffset):
|
||||
"""Adjust FOV according to vertical scroll."""
|
||||
nonlocal fov
|
||||
fov += yoffset
|
||||
if fov < FOV_MIN: fov = FOV_MIN
|
||||
if fov > FOV_MAX: fov = FOV_MAX
|
||||
glfw.set_scroll_callback(window, zoom)
|
||||
|
||||
mapid = list(range(48))
|
||||
shuffle(mapid)
|
||||
view = View(mapid, context)
|
||||
mypico = view.camera
|
||||
glfw.set_cursor_pos_callback(window, mypico.look)
|
||||
|
||||
last_time = glfw.get_time()
|
||||
while not glfw.window_should_close(window):
|
||||
next_time = glfw.get_time()
|
||||
mypico.fps = 1 / (next_time-last_time)
|
||||
last_time = next_time
|
||||
|
||||
if glfw.get_key(window, glfw.KEY_UP) == glfw.PRESS:
|
||||
mypico.move(forward=1)
|
||||
if glfw.get_key(window, glfw.KEY_DOWN) == glfw.PRESS:
|
||||
mypico.move(forward=-1)
|
||||
if glfw.get_key(window, glfw.KEY_LEFT) == glfw.PRESS:
|
||||
mypico.move(right=-1)
|
||||
if glfw.get_key(window, glfw.KEY_RIGHT) == glfw.PRESS:
|
||||
mypico.move(right=1)
|
||||
|
||||
width, height = glfw.get_window_size(window)
|
||||
context.viewport = 0, 0, width, height
|
||||
context.clear(*color('Background'))
|
||||
view.render(width, height, fov)
|
||||
|
||||
glfw.swap_buffers(window)
|
||||
glfw.set_cursor_pos(window, width/2, height/2)
|
||||
glfw.poll_events()
|
||||
|
||||
glfw.terminate()
|
45
axuy/misc.py
45
axuy/misc.py
|
@ -17,9 +17,10 @@
|
|||
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from itertools import chain, combinations_with_replacement, permutations
|
||||
from random import choices, shuffle
|
||||
|
||||
import numpy
|
||||
import pkg_resources
|
||||
from pkg_resources import resource_filename
|
||||
|
||||
TANGO = {'Background': '2e3436',
|
||||
'Butter': 'fce94f',
|
||||
|
@ -30,22 +31,50 @@ TANGO = {'Background': '2e3436',
|
|||
'Plum': 'ad7fa8',
|
||||
'Scarlet Red': 'ef2929',
|
||||
'Aluminium': 'eeeeec'}
|
||||
NEIGHBORS = set(chain.from_iterable(
|
||||
map(permutations, combinations_with_replacement((-1, 0, 1), 3))))
|
||||
# map.npy is generated by ../tools/mapgen
|
||||
SPACE = numpy.load(resource_filename('axuy', 'map.npy'))
|
||||
|
||||
|
||||
def abspath(resource_name):
|
||||
"""Return a true filesystem path for the specified resource."""
|
||||
return resource_filename('axuy', resource_name)
|
||||
|
||||
|
||||
def color(name):
|
||||
"""Return numpy float32 array of RGB colors from color name."""
|
||||
"""Return NumPy float32 array of RGB colors from color name."""
|
||||
return numpy.float32([i / 255 for i in bytes.fromhex(TANGO[name])])
|
||||
|
||||
|
||||
def mapidgen(replacement=False):
|
||||
"""Return a randomly generated map ID."""
|
||||
mapid = list(range(48))
|
||||
if replacement: return choices(mapid, k=48)
|
||||
shuffle(mapid)
|
||||
return mapid
|
||||
|
||||
|
||||
def mapgen(mapid):
|
||||
"""Return the NumPy array of shape (12, 12, 9) of bools
|
||||
generated from the given ID.
|
||||
"""
|
||||
base = numpy.stack([SPACE[i] for i in mapid]).reshape(4, 4, 3, 3, 3, 3)
|
||||
space = numpy.zeros([12, 12, 9], dtype=bool)
|
||||
for (i, j, k, x, y, z), occupied in numpy.ndenumerate(base):
|
||||
if occupied: space[i*3 + x][j*3 + y][k*3 + z] = 1
|
||||
return space
|
||||
|
||||
|
||||
def neighbors(x, y, z):
|
||||
"""Return a generator of coordinates of images point (x, y, z)
|
||||
in neighbor universes.
|
||||
"""
|
||||
for i, j, k in set(chain.from_iterable(
|
||||
map(permutations, combinations_with_replacement((-1, 0, 1), 3)))):
|
||||
yield x + i*12, y + j*12, z + k*9
|
||||
for i, j, k in NEIGHBORS: yield x + i*12, y + j*12, z + k*9
|
||||
|
||||
|
||||
def resource_filename(resource_name):
|
||||
"""Return a true filesystem path for the specified resource."""
|
||||
return pkg_resources.resource_filename('axuy', resource_name)
|
||||
def sign(x):
|
||||
"""Return the sign of number x."""
|
||||
if x > 0: return 1
|
||||
if x: return -1
|
||||
return 0
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
# peer.py - peer main loop
|
||||
# Copyright (C) 2019 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of Axuy
|
||||
#
|
||||
# Axuy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Axuy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
__doc__ = 'Axuy main loop'
|
||||
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from pickle import dumps, loads
|
||||
from socket import socket, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR
|
||||
from threading import Thread
|
||||
|
||||
from .misc import mapgen, mapidgen
|
||||
from .pico import Picobot
|
||||
from .view import View
|
||||
|
||||
|
||||
class Peer:
|
||||
"""Axuy peer.
|
||||
TODO: Documentation
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
if args.seeder is None:
|
||||
mapid = mapidgen()
|
||||
self.peers = []
|
||||
else:
|
||||
client = socket()
|
||||
host, port = args.seeder.split(':')
|
||||
self.peers = [(host, int(port))]
|
||||
client.connect(*self.peers)
|
||||
data = loads(client.recv(1024))
|
||||
mapid = data['mapid']
|
||||
self.peers.extend(data['peers'])
|
||||
|
||||
self.space = mapgen(mapid)
|
||||
self.pico = Picobot(self.space, (0, 0, 0))
|
||||
self.view = View(self.pico, args.width, args.height, self.space)
|
||||
|
||||
address = args.host, args.port
|
||||
data_server = Thread(target=self.serve,
|
||||
args=(address, mapid))
|
||||
data_server.daemon = True
|
||||
data_server.start()
|
||||
|
||||
self.sock = socket(type=SOCK_DGRAM) # UDP
|
||||
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
self.sock.bind(address)
|
||||
|
||||
pusher = Thread(target=self.push)
|
||||
pusher.daemon = True
|
||||
pusher.start()
|
||||
puller = Thread(target=self.pull)
|
||||
puller.daemon = True
|
||||
puller.start()
|
||||
|
||||
def serve(self, address, mapid):
|
||||
"""Initiate peers."""
|
||||
self.server = socket() # TCP server
|
||||
self.server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
self.server.bind(address)
|
||||
self.server.listen(7)
|
||||
while self.view.is_running:
|
||||
conn, addr = self.server.accept()
|
||||
conn.send(dumps({'mapid': mapid, 'peers': self.peers}))
|
||||
conn.close()
|
||||
self.server.close()
|
||||
|
||||
def push(self):
|
||||
"""Send own state to peers."""
|
||||
while self.view.is_running:
|
||||
for peer in self.peers:
|
||||
self.sock.sendto(dumps(self.view.camera.state), peer)
|
||||
|
||||
def pull(self):
|
||||
"""Receive peers' state."""
|
||||
while self.view.is_running:
|
||||
data, addr = self.sock.recvfrom(1024)
|
||||
pos, rot = loads(data)
|
||||
try:
|
||||
self.view.picos[addr].update(pos, rot)
|
||||
except KeyError:
|
||||
self.view.picos[addr] = Picobot(self.space, pos, rot)
|
||||
self.peers.append(addr)
|
||||
|
||||
def __enter__(self): return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.server.close()
|
||||
self.sock.close()
|
||||
self.view.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse command-line arguments and start main loop."""
|
||||
parser = ArgumentParser(usage='%(prog)s [options]',
|
||||
formatter_class=RawTextHelpFormatter)
|
||||
parser.add_argument('--seeder')
|
||||
parser.add_argument('--host')
|
||||
parser.add_argument('--port', type=int)
|
||||
parser.add_argument('--width', type=int, help='window width')
|
||||
parser.add_argument('--height', type=int, help='window height')
|
||||
with Peer(parser.parse_args()) as peer:
|
||||
while peer.view.is_running: peer.view.update()
|
41
axuy/pico.py
41
axuy/pico.py
|
@ -26,6 +26,8 @@ import numpy as np
|
|||
from pyrr import matrix33
|
||||
|
||||
BASE = np.float32([[1, 0, 0], [0, 1, 0], [0, 0, -1]])
|
||||
RP = 1 / 4 # radius of a Picobot
|
||||
|
||||
SPEED = 2
|
||||
MOUSE_SPEED = 1/8
|
||||
|
||||
|
@ -54,15 +56,15 @@ class Picobot:
|
|||
Currently rendered frames per second.
|
||||
"""
|
||||
|
||||
def __init__(self, space, pos=None, rotation=None):
|
||||
def __init__(self, space, position=None, rotation=None):
|
||||
self.space = space
|
||||
if pos is None:
|
||||
if position is None:
|
||||
x, y, z = random()*12, random()*12, random()*9
|
||||
while not self.empty(x, y, z):
|
||||
x, y, z = random()*12, random()*12, random()*9
|
||||
self.x, self.y, self.z = x, y, z
|
||||
else:
|
||||
self.x, self.y, self.z = pos
|
||||
self.x, self.y, self.z = position
|
||||
|
||||
if rotation is None:
|
||||
self.rotation = BASE
|
||||
|
@ -72,14 +74,19 @@ class Picobot:
|
|||
|
||||
self.fps = 60.0
|
||||
|
||||
def update(self, position, rotation):
|
||||
"""Update state."""
|
||||
self.pos = position
|
||||
self.rotation = rotation
|
||||
|
||||
def empty(self, x, y, z) -> bool:
|
||||
"""Return weather a Picobot can be placed at (x, y, z)."""
|
||||
if self.space[int((x-1/4) % 12)][int(y % 12)][int(z % 9)]: return False
|
||||
if self.space[int((x+1/4) % 12)][int(y % 12)][int(z % 9)]: return False
|
||||
if self.space[int(x % 12)][int((y-1/4) % 12)][int(z % 9)]: return False
|
||||
if self.space[int(x % 12)][int((y+1/4) % 12)][int(z % 9)]: return False
|
||||
if self.space[int(x % 12)][int(y % 12)][int((z-1/4) % 9)]: return False
|
||||
if self.space[int(x % 12)][int(y % 12)][int((z+1/4) % 9)]: return False
|
||||
"""Return whether a Picobot can be placed at (x, y, z)."""
|
||||
if self.space[int((x-RP) % 12)][int(y % 12)][int(z % 9)]: return False
|
||||
if self.space[int((x+RP) % 12)][int(y % 12)][int(z % 9)]: return False
|
||||
if self.space[int(x % 12)][int((y-RP) % 12)][int(z % 9)]: return False
|
||||
if self.space[int(x % 12)][int((y+RP) % 12)][int(z % 9)]: return False
|
||||
if self.space[int(x % 12)][int(y % 12)][int((z-RP) % 9)]: return False
|
||||
if self.space[int(x % 12)][int(y % 12)][int((z+RP) % 9)]: return False
|
||||
return True
|
||||
|
||||
def rotate(self, yaw, pitch):
|
||||
|
@ -97,6 +104,20 @@ class Picobot:
|
|||
if self.empty(self.x, y, self.z): self.y = y % 12
|
||||
if self.empty(self.x, self.y, z): self.z = z % 9
|
||||
|
||||
@property
|
||||
def pos(self):
|
||||
"""Position in a NumPy array."""
|
||||
return np.float32([self.x, self.y, self.z])
|
||||
|
||||
@pos.setter
|
||||
def pos(self, position):
|
||||
self.x, self.y, self.z = position
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Position and rotation."""
|
||||
return self.pos, self.rotation
|
||||
|
||||
def look(self, window, xpos, ypos):
|
||||
"""Look according to cursor position.
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#version 330
|
||||
|
||||
uniform vec3 color;
|
||||
|
||||
in float depth;
|
||||
out vec4 f_color;
|
||||
|
||||
void main() {
|
||||
f_color = vec4(color, 1 - depth);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
#version 330
|
||||
|
||||
uniform mat4 vp;
|
||||
uniform mat4 model;
|
||||
uniform vec3 camera;
|
||||
uniform float visibility;
|
||||
|
||||
in vec3 in_vert;
|
||||
out float depth;
|
||||
|
||||
void main() {
|
||||
vec4 vert = model * vec4(in_vert, 1.0);
|
||||
gl_Position = vp * vert;
|
||||
depth = distance(camera, vec3(vert)) / visibility;
|
||||
}
|
189
axuy/view.py
189
axuy/view.py
|
@ -18,18 +18,19 @@
|
|||
|
||||
__doc__ = 'Axuy module for map class'
|
||||
|
||||
from itertools import product, starmap
|
||||
from operator import add
|
||||
from itertools import product
|
||||
from math import sqrt
|
||||
|
||||
import glfw
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from pyrr import Matrix44
|
||||
|
||||
from .misc import color, neighbors, resource_filename
|
||||
from .pico import Picobot
|
||||
from .misc import abspath, color, neighbors, sign
|
||||
|
||||
# map.npy is generated by ../tools/mapgen
|
||||
SPACE = np.load(resource_filename('map.npy'))
|
||||
FOV_MIN = 30
|
||||
FOV_MAX = 120
|
||||
FOV_INIT = (FOV_MIN+FOV_MAX) / 2
|
||||
|
||||
OXY = np.float32([[0, 0, 0], [1, 0, 0], [1, 1, 0],
|
||||
[1, 1, 0], [0, 1, 0], [0, 0, 0]])
|
||||
|
@ -37,25 +38,25 @@ OYZ = np.float32([[0, 0, 0], [0, 1, 0], [0, 1, 1],
|
|||
[0, 1, 1], [0, 0, 1], [0, 0, 0]])
|
||||
OZX = np.float32([[0, 0, 0], [1, 0, 0], [1, 0, 1],
|
||||
[1, 0, 1], [0, 0, 1], [0, 0, 0]])
|
||||
OCTAHEDRON = np.float32([[+1/4, 0, 0], [0, +1/4, 0], [0, 0, +1/4],
|
||||
[+1/4, 0 ,0], [0, +1/4, 0], [0, 0, -1/4],
|
||||
[+1/4, 0 ,0], [0, -1/4, 0], [0, 0, +1/4],
|
||||
[+1/4, 0 ,0], [0, -1/4, 0], [0, 0, -1/4],
|
||||
[-1/4, 0 ,0], [0, +1/4, 0], [0, 0, +1/4],
|
||||
[-1/4, 0 ,0], [0, +1/4, 0], [0, 0, -1/4],
|
||||
[-1/4, 0 ,0], [0, -1/4, 0], [0, 0, +1/4],
|
||||
[-1/4, 0 ,0], [0, -1/4, 0], [0, 0, -1/4]])
|
||||
TETRAHEDRON = np.float32([[+1, +1, +1], [+1, -1, -1], [-1, +1, -1],
|
||||
[-1, -1, +1], [+1, -1, -1], [-1, +1, -1],
|
||||
[+1, +1, +1], [-1, -1, +1], [-1, +1, -1],
|
||||
[+1, +1, +1], [-1, -1, +1], [+1, -1, -1]])
|
||||
|
||||
with open(resource_filename('space.vert')) as f: VERTEX_SHADER = f.read()
|
||||
with open(resource_filename('space.frag')) as f: FRAGMENT_SHADER = f.read()
|
||||
TETRAVERTICES = np.float32([[0, sqrt(8), -1], [sqrt(6), -sqrt(2), -1],
|
||||
[0, 0, 3], [-sqrt(6), -sqrt(2), -1]]) / 12
|
||||
TETRAINDECIES = np.int32([0, 1, 2, 3, 1, 2, 0, 3, 2, 0, 3, 1])
|
||||
|
||||
OCTOVERTICES = np.float32([[-1, 0, 0], [0, -1, 0], [0, 0, -1],
|
||||
[0, 1, 0], [0, 0, 1], [1, 0, 0]]) / 12
|
||||
OCTOINDECIES = np.int32([0, 1, 2, 0, 1, 4, 3, 0, 2, 3, 0, 4,
|
||||
2, 1, 5, 4, 1, 5, 2, 5, 3, 4, 5, 3])
|
||||
|
||||
with open(abspath('shaders/map.vert')) as f: MAP_VERTEX = f.read()
|
||||
with open(abspath('shaders/map.frag')) as f: MAP_FRAGMENT = f.read()
|
||||
with open(abspath('shaders/pico.vert')) as f: PICO_VERTEX = f.read()
|
||||
with open(abspath('shaders/pico.frag')) as f: PICO_FRAGMENT = f.read()
|
||||
|
||||
|
||||
class View:
|
||||
"""World map and camera placement.
|
||||
(Documentation below is not completed.)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
@ -68,7 +69,7 @@ class View:
|
|||
----------
|
||||
space : np.ndarray of shape (12, 12, 9) of bools
|
||||
3D array of occupied space.
|
||||
prog : moderngl.Program
|
||||
maprog : moderngl.Program
|
||||
Processed executable code in GLSL.
|
||||
mapva : moderngl.VertexArray
|
||||
Vertex data of the map.
|
||||
|
@ -76,33 +77,70 @@ class View:
|
|||
Protagonist whose view is the camera.
|
||||
"""
|
||||
|
||||
def __init__(self, mapid, context):
|
||||
space = np.stack([SPACE[i] for i in mapid]).reshape(4, 4, 3, 3, 3, 3)
|
||||
self.space = np.zeros([12, 12, 9], dtype=bool)
|
||||
for (i, j, k, x, y, z), occupied in np.ndenumerate(space):
|
||||
if occupied: self.space[i*3 + x][j*3 + y][k*3 + z] = 1
|
||||
def __init__(self, pico, width, height, space):
|
||||
# Create GLFW window
|
||||
if not glfw.init(): raise RuntimeError('Failed to initialize GLFW!')
|
||||
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
|
||||
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
|
||||
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
|
||||
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
|
||||
self.window = glfw.create_window(width, height, 'Axuy', None, None)
|
||||
if not self.window:
|
||||
glfw.terminate()
|
||||
raise RuntimeError('Failed to create glfw window!')
|
||||
|
||||
vertices = []
|
||||
self.camera = pico
|
||||
self.picos = {'self': pico}
|
||||
self.last_time = glfw.get_time() # to keep track of FPS
|
||||
|
||||
# Window's rendering and event-handling configuration
|
||||
glfw.make_context_current(self.window)
|
||||
glfw.swap_interval(1)
|
||||
glfw.set_input_mode(self.window, glfw.CURSOR, glfw.CURSOR_DISABLED)
|
||||
glfw.set_input_mode(self.window, glfw.STICKY_KEYS, True)
|
||||
glfw.set_cursor_pos_callback(self.window, self.camera.look)
|
||||
self.fov = FOV_INIT
|
||||
glfw.set_scroll_callback(self.window, self.zoom)
|
||||
|
||||
# Create OpenGL context
|
||||
self.context = context = moderngl.create_context()
|
||||
context.enable(moderngl.BLEND)
|
||||
context.enable(moderngl.DEPTH_TEST)
|
||||
|
||||
self.space, vertices = space, []
|
||||
for (x, y, z), occupied in np.ndenumerate(self.space):
|
||||
if self.space[x][y][z-1] ^ occupied:
|
||||
vertices.extend(starmap(add, product(neighbors(x, y, z), OXY)))
|
||||
vertices.extend(i+j for i,j in product(neighbors(x,y,z), OXY))
|
||||
if self.space[x-1][y][z] ^ occupied:
|
||||
vertices.extend(starmap(add, product(neighbors(x, y, z), OYZ)))
|
||||
vertices.extend(i+j for i,j in product(neighbors(x,y,z), OYZ))
|
||||
if self.space[x][y-1][z] ^ occupied:
|
||||
vertices.extend(starmap(add, product(neighbors(x, y, z), OZX)))
|
||||
vertices.extend(i+j for i,j in product(neighbors(x,y,z), OZX))
|
||||
|
||||
self.prog = context.program(vertex_shader=VERTEX_SHADER,
|
||||
fragment_shader=FRAGMENT_SHADER)
|
||||
self.prog['bg'].write(color('Background').tobytes())
|
||||
self.maprog = context.program(vertex_shader=MAP_VERTEX,
|
||||
fragment_shader=MAP_FRAGMENT)
|
||||
self.maprog['bg'].write(color('Background').tobytes())
|
||||
self.maprog['color'].write(color('Aluminium').tobytes())
|
||||
mapvb = context.buffer(np.stack(vertices).astype(np.float32).tobytes())
|
||||
self.mapva = context.simple_vertex_array(self.prog, mapvb, 'in_vert')
|
||||
self.mapva = context.simple_vertex_array(self.maprog, mapvb, 'in_vert')
|
||||
|
||||
self.camera = Picobot(self.space)
|
||||
self.prog = context.program(vertex_shader=PICO_VERTEX,
|
||||
fragment_shader=PICO_FRAGMENT)
|
||||
pvb = [(context.buffer(TETRAVERTICES.tobytes()), '3f', 'in_vert')]
|
||||
pib = context.buffer(TETRAINDECIES.tobytes())
|
||||
self.pva = context.vertex_array(self.prog, pvb, pib)
|
||||
|
||||
self.should_close = None
|
||||
|
||||
def zoom(self, window, xoffset, yoffset):
|
||||
"""Adjust FOV according to vertical scroll."""
|
||||
self.fov += yoffset
|
||||
if self.fov < FOV_MIN: self.fov = FOV_MIN
|
||||
if self.fov > FOV_MAX: self.fov = FOV_MAX
|
||||
|
||||
@property
|
||||
def pos(self):
|
||||
"""Camera position in a NumPy array."""
|
||||
return np.float32([self.camera.x, self.camera.y, self.camera.z])
|
||||
return self.camera.pos
|
||||
|
||||
@property
|
||||
def right(self):
|
||||
|
@ -119,17 +157,76 @@ class View:
|
|||
"""Camera forward direction."""
|
||||
return self.camera.rotation[2]
|
||||
|
||||
def render(self, width, height, fov):
|
||||
"""Render the map."""
|
||||
visibility = 360 / fov
|
||||
self.prog['visibility'].write(np.float32(visibility).tobytes())
|
||||
self.prog['camera'].write(np.float32(self.pos).tobytes())
|
||||
@property
|
||||
def is_running(self):
|
||||
"""GLFW window status."""
|
||||
return not glfw.window_should_close(self.window)
|
||||
|
||||
projection = Matrix44.perspective_projection(fov, width/height,
|
||||
9e-6, visibility)
|
||||
def is_pressed(self, *keys):
|
||||
"""Return whether given keys are pressed."""
|
||||
return any(glfw.get_key(self.window, k) == glfw.PRESS for k in keys)
|
||||
|
||||
def render(self, obj, va):
|
||||
"""Render the obj and its images in bounded 3D space."""
|
||||
rotation = Matrix44.from_matrix33(obj.rotation)
|
||||
i, j, k = map(sign, self.pos - obj.pos)
|
||||
for position in product(*zip(obj.pos, obj.pos + [i*12, j*12, k*9])):
|
||||
model = rotation @ Matrix44.from_translation(position)
|
||||
self.prog['model'].write(model.astype(np.float32).tobytes())
|
||||
self.prog['color'].write(color('Background').tobytes())
|
||||
va.render(moderngl.LINES)
|
||||
self.prog['color'].write(color('Plum').tobytes())
|
||||
va.render(moderngl.TRIANGLES)
|
||||
|
||||
def render_pico(self, pico):
|
||||
"""Render the pico and its images in bounded 3D space."""
|
||||
rotation = Matrix44.from_matrix33(pico.rotation)
|
||||
i, j, k = map(sign, self.pos - pico.pos)
|
||||
for position in product(*zip(pico.pos, pico.pos + [i*12, j*12, k*9])):
|
||||
model = rotation @ Matrix44.from_translation(position)
|
||||
self.prog['model'].write(model.astype(np.float32).tobytes())
|
||||
self.prog['color'].write(color('Background').tobytes())
|
||||
self.pva.render(moderngl.LINES)
|
||||
self.prog['color'].write(color('Plum').tobytes())
|
||||
self.pva.render(moderngl.TRIANGLES)
|
||||
|
||||
def update(self):
|
||||
"""Handle input, update GLSL programs and render the map."""
|
||||
# Character movements
|
||||
right, upward, forward = 0, 0, 0
|
||||
if self.is_pressed(glfw.KEY_UP): forward += 1
|
||||
if self.is_pressed(glfw.KEY_DOWN): forward -= 1
|
||||
if self.is_pressed(glfw.KEY_LEFT): right -= 1
|
||||
if self.is_pressed(glfw.KEY_RIGHT): right += 1
|
||||
self.camera.move(right, upward, forward)
|
||||
|
||||
# Renderings
|
||||
width, height = glfw.get_window_size(self.window)
|
||||
self.context.viewport = 0, 0, width, height
|
||||
self.context.clear(*color('Background'))
|
||||
|
||||
visibility = sqrt(1800 / self.fov)
|
||||
projection = Matrix44.perspective_projection(self.fov, width/height,
|
||||
3E-3, visibility)
|
||||
view = Matrix44.look_at(self.pos, self.pos + self.forward, self.upward)
|
||||
vp = view @ projection
|
||||
vp = (view @ projection).astype(np.float32).tobytes()
|
||||
|
||||
self.prog['mvp'].write(vp.astype(np.float32).tobytes())
|
||||
self.prog['color'].write(color('Aluminium').tobytes())
|
||||
self.maprog['visibility'].write(np.float32(visibility).tobytes())
|
||||
self.maprog['camera'].write(self.pos.tobytes())
|
||||
self.maprog['mvp'].write(vp)
|
||||
self.mapva.render(moderngl.TRIANGLES)
|
||||
|
||||
self.prog['visibility'].write(np.float32(visibility).tobytes())
|
||||
self.prog['camera'].write(self.pos.tobytes())
|
||||
self.prog['vp'].write(vp)
|
||||
for pico in self.picos.copy().values():
|
||||
if pico is not self.camera: self.render_pico(pico)
|
||||
glfw.swap_buffers(self.window)
|
||||
|
||||
# Resetting cursor position and event queues
|
||||
glfw.set_cursor_pos(self.window, width/2, height/2)
|
||||
glfw.poll_events()
|
||||
|
||||
def close(self):
|
||||
"""Close window."""
|
||||
glfw.terminate()
|
||||
|
|
4
setup.py
4
setup.py
|
@ -28,5 +28,5 @@ setup(
|
|||
keywords='fps opengl glfw',
|
||||
packages=['axuy'],
|
||||
install_requires=['numpy', 'pyrr', 'moderngl', 'glfw'],
|
||||
package_data={'axuy': ['map.npy', 'space.vert', 'space.frag']},
|
||||
entry_points={'console_scripts': ['axuy = axuy.main:main']})
|
||||
package_data={'axuy': ['map.npy', 'shaders']},
|
||||
entry_points={'console_scripts': ['axuy = axuy.peer:main']})
|
||||
|
|
Loading…
Reference in New Issue